[
  {
    "path": ".github/workflows/main.yml",
    "content": "name: ci\non: [\"push\", \"pull_request\", \"workflow_dispatch\"]\n\njobs:\n\n  lint:\n    name: lint\n    runs-on: ubuntu-20.04\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          python-version: '3.9'\n      - name: extract pip cache\n        uses: actions/cache@v4\n        with:\n          path: ~/.cache/pip\n          key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}\n          restore-keys: ${{ runner.os }}-pip-\n      - run: pip install --user --upgrade pip wheel\n      - run: pip install -e .[lint]\n      - run: make lint\n\n  tests-unit:\n    name: \"tests / unit\"\n    strategy:\n      matrix:\n        os:\n          - ubuntu-20.04\n          - macos-13\n          - windows-2022\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          python-version: '3.9'\n      - name: set pip cache dir\n        shell: bash\n        run: echo \"PIP_CACHE_DIR=$(pip cache dir)\" >> $GITHUB_ENV\n      - name: extract pip cache\n        uses: actions/cache@v4\n        with:\n          path: ${{ env.PIP_CACHE_DIR }}\n          key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}\n          restore-keys: ${{ runner.os }}-pip-\n      - id: os-name\n        uses: ASzc/change-string-case-action@v6\n        with:\n          string: ${{ runner.os }}\n      - run: python -m pip install --user --upgrade pip wheel\n      - if: startsWith(runner.os, 'linux')\n        run: pip install -e .[test]\n      - if: startsWith(runner.os, 'linux')\n        env:\n          HOME: /tmp\n        run: make test-unit-coverage\n      - if: startsWith(runner.os, 'linux') != true\n        run: pip install -e .[test]\n      - if: startsWith(runner.os, 'linux') != true\n        env:\n          HOME: /tmp\n        run: coverage run --source=lbry -m unittest tests/unit/test_conf.py\n      - name: submit coverage report\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          COVERALLS_FLAG_NAME: tests-unit-${{ steps.os-name.outputs.lowercase }}\n          COVERALLS_PARALLEL: true\n        run: |\n          pip install coveralls\n          coveralls --service=github\n\n  tests-integration:\n    name: \"tests / integration\"\n    runs-on: ubuntu-20.04\n    strategy:\n      matrix:\n        test:\n          - datanetwork\n          - blockchain\n          - claims\n          - takeovers\n          - transactions\n          - other\n    steps:\n      - name: Configure sysctl limits\n        run: |\n          sudo swapoff -a\n          sudo sysctl -w vm.swappiness=1\n          sudo sysctl -w fs.file-max=262144\n          sudo sysctl -w vm.max_map_count=262144\n      - name: Runs Elasticsearch\n        uses: elastic/elastic-github-actions/elasticsearch@master\n        with:\n          stack-version: 7.12.1\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          python-version: '3.9'\n      - if: matrix.test == 'other'\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y --no-install-recommends ffmpeg\n      - name: extract pip cache\n        uses: actions/cache@v4\n        with:\n          path: ./.tox\n          key: tox-integration-${{ matrix.test }}-${{ hashFiles('setup.py') }}\n          restore-keys: txo-integration-${{ matrix.test }}-\n      - run: pip install tox coverage coveralls\n      - if: matrix.test == 'claims'\n        run: rm -rf .tox\n      - run: tox -e ${{ matrix.test }}\n      - name: submit coverage report\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          COVERALLS_FLAG_NAME: tests-integration-${{ matrix.test }}\n          COVERALLS_PARALLEL: true\n        run: |\n          coverage combine tests\n          coveralls --service=github\n\n\n  coverage:\n    needs: [\"tests-unit\", \"tests-integration\"]\n    runs-on: ubuntu-20.04\n    steps:\n      - name: finalize coverage report submission\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          pip install coveralls\n          coveralls --service=github --finish\n\n  build:\n    needs: [\"lint\", \"tests-unit\", \"tests-integration\"]\n    name: \"build / binary\"\n    strategy:\n      matrix:\n        os:\n          - ubuntu-20.04\n          - macos-13\n          - windows-2022\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          python-version: '3.9'\n      - id: os-name\n        uses: ASzc/change-string-case-action@v6\n        with:\n          string: ${{ runner.os }}\n      - name: set pip cache dir\n        shell: bash\n        run: echo \"PIP_CACHE_DIR=$(pip cache dir)\" >> $GITHUB_ENV\n      - name: extract pip cache\n        uses: actions/cache@v4\n        with:\n          path: ${{ env.PIP_CACHE_DIR }}\n          key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}\n          restore-keys: ${{ runner.os }}-pip-\n      - run: pip install pyinstaller==6.0\n      - run: pip install -e .\n      - if: startsWith(github.ref, 'refs/tags/v')\n        run: python docker/set_build.py\n      - if: startsWith(runner.os, 'linux') || startsWith(runner.os, 'mac')\n        name: Build & Run (Unix)\n        run: |\n          pyinstaller --onefile --name lbrynet lbry/extras/cli.py\n          dist/lbrynet --version\n      - if: startsWith(runner.os, 'windows')\n        name: Build & Run (Windows)\n        run: |\n          pip install pywin32==301\n          pyinstaller --additional-hooks-dir=scripts/. --icon=icons/lbry256.ico --onefile --name lbrynet lbry/extras/cli.py\n          dist/lbrynet.exe --version\n      - uses: actions/upload-artifact@v4\n        with:\n          name: lbrynet-${{ steps.os-name.outputs.lowercase }}\n          path: dist/\n\n  release:\n    name: \"release\"\n    if: startsWith(github.ref, 'refs/tags/v')\n    needs: [\"build\"]\n    runs-on: ubuntu-20.04\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/download-artifact@v4\n      - name: upload binaries\n        env:\n          GITHUB_TOKEN: ${{ secrets.RELEASE_API_TOKEN }}\n        run: |\n          pip install githubrelease\n          chmod +x lbrynet-macos/lbrynet\n          chmod +x lbrynet-linux/lbrynet\n          zip --junk-paths lbrynet-mac.zip lbrynet-macos/lbrynet\n          zip --junk-paths lbrynet-linux.zip lbrynet-linux/lbrynet\n          zip --junk-paths lbrynet-windows.zip lbrynet-windows/lbrynet.exe\n          ls -lh\n          githubrelease release lbryio/lbry-sdk info ${GITHUB_REF#refs/tags/}\n          githubrelease asset lbryio/lbry-sdk upload ${GITHUB_REF#refs/tags/} \\\n            lbrynet-mac.zip lbrynet-linux.zip lbrynet-windows.zip\n          githubrelease release lbryio/lbry-sdk publish ${GITHUB_REF#refs/tags/}\n\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: slack\n\non:\n  release:\n    types: [published]\n\njobs:\n  release:\n    name: \"slack notification\"\n    runs-on: ubuntu-20.04\n    steps:\n      - uses: LoveToKnow/slackify-markdown-action@v1.0.0\n        id: markdown\n        with:\n          text: \"There is a new SDK release: ${{github.event.release.html_url}}\\n${{ github.event.release.body }}\"\n      - uses: slackapi/slack-github-action@v1.14.0\n        env:\n          CHANGELOG: '<!channel> ${{ steps.markdown.outputs.text }}'\n          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_RELEASE_BOT_WEBHOOK }}\n        with:\n          payload: '{\"type\": \"mrkdwn\", \"text\": ${{ toJSON(env.CHANGELOG) }} }'\n\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Change Log\n\nThis changelog is no longer up to date. For release notes, see https://github.com/lbryio/lbry-sdk/releases.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "## Contributing to LBRY\n\nhttps://lbry.tech/contribute\n"
  },
  {
    "path": "INSTALL.md",
    "content": "# Installing LBRY\n\nIf 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)!\n\nThese 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).\n\nHere'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):\n[![Setup for development](https://spee.ch/2018-10-04-17-13-54-017046806.png)](https://spee.ch/967f99344308f1e90f0620d91b6c93e4dfb240e0/lbrynet-dev-setup.mp4)\n\n## Prerequisites\n\nRunning `lbrynet` from source requires Python 3.7. Get the installer for your OS [here](https://www.python.org/downloads/release/python-370/).\n\nAfter installing Python 3.7, you'll need to install some additional libraries depending on your operating system.\n\nBecause of [issue #2769](https://github.com/lbryio/lbry-sdk/issues/2769)\nat the moment the `lbrynet` daemon will only work correctly with Python 3.7.\nIf Python 3.8+ is used, the daemon will start but the RPC server\nmay not accept messages, returning the following:\n```\nCould not connect to daemon. Are you sure it's running?\n```\n\n### macOS\n\nmacOS 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/).\n\nThese environment variables also need to be set:\n```\nPYTHONUNBUFFERED=1\nEVENT_NOKQUEUE=1\n```\n\nRemaining dependencies can then be installed by running:\n```\nbrew install python protobuf\n```\n\nAssistance installing Python3: https://docs.python-guide.org/starting/install3/osx/.\n\n### Linux\n\nOn Ubuntu (we recommend 18.04 or 20.04), install the following:\n```\nsudo add-apt-repository ppa:deadsnakes/ppa\nsudo apt-get update\nsudo apt-get install build-essential python3.7 python3.7-dev git python3.7-venv libssl-dev python-protobuf\n```\n\nThe [deadsnakes PPA](https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa) provides Python 3.7\nfor those Ubuntu distributions that no longer have it in their\nofficial repositories.\n\nOn Raspbian, you will also need to install `python-pyparsing`.\n\nIf you're running another Linux distro, install the equivalent of the above packages for your system.\n\n## Installation\n\n### Linux/Mac\n\nClone the repository:\n```bash\ngit clone https://github.com/lbryio/lbry-sdk.git\ncd lbry-sdk\n```\n\nCreate a Python virtual environment for lbry-sdk:\n```bash\npython3.7 -m venv lbry-venv\n```\n\nActivate virtual environment:\n```bash\nsource lbry-venv/bin/activate\n```\n\nMake sure you're on Python 3.7+ as default in the virtual environment:\n```bash\npython --version\n```\n\nInstall packages:\n```bash\nmake install\n```\n\nIf you are on Linux and using PyCharm, generates initial configs:\n```bash\nmake idea\n```\n\nTo verify your installation, `which lbrynet` should return a path inside\nof the `lbry-venv` folder.\n```bash\n(lbry-venv) $ which lbrynet\n/opt/lbry-sdk/lbry-venv/bin/lbrynet\n```\n\nTo exit the virtual environment simply use the command `deactivate`.\n\n### Windows\n\nClone the repository:\n```bash\ngit clone https://github.com/lbryio/lbry-sdk.git\ncd lbry-sdk\n```\n\nCreate a Python virtual environment for lbry-sdk:\n```bash\npython -m venv lbry-venv\n```\n\nActivate virtual environment:\n```bash\nlbry-venv\\Scripts\\activate\n```\n\nInstall packages:\n```bash\npip install -e .\n```\n\n## Run the tests\n### Elasticsearch\n\nFor running integration tests, Elasticsearch is required to be available at localhost:9200/\n\nThe easiest way to start it is using docker with:\n```bash\nmake elastic-docker\n```\n\nAlternative installation methods are available [at Elasticsearch website](https://www.elastic.co/guide/en/elasticsearch/reference/current/install-elasticsearch.html).\n\nTo run the unit and integration tests from the repo directory:\n```\npython -m unittest discover tests.unit\npython -m unittest discover tests.integration\n```\n\n## Usage\n\nTo start the API server:\n```\nlbrynet start\n```\n\nWhenever the code inside [lbry-sdk/lbry](./lbry)\nis modified we should run `make install` to recompile the `lbrynet`\nexecutable with the newest code.\n\n## Development\n\nWhen developing, remember to enter the environment,\nand if you wish start the server interactively.\n```bash\n$ source lbry-venv/bin/activate\n\n(lbry-venv) $ python lbry/extras/cli.py start\n```\n\nParameters can be passed in the same way.\n```bash\n(lbry-venv) $ python lbry/extras/cli.py wallet balance\n```\n\nIf a Python debugger (`pdb` or `ipdb`) is installed we can also start it\nin this way, set up break points, and step through the code.\n```bash\n(lbry-venv) $ pip install ipdb\n\n(lbry-venv) $ ipdb lbry/extras/cli.py\n```\n\nHappy hacking!\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015-2022 LBRY Inc\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the\nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include README.md\ninclude CHANGELOG.md\ninclude LICENSE\nrecursive-include lbry *.txt *.py\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: install tools lint test test-unit test-unit-coverage test-integration idea\n\ninstall:\n\tpip install -e .\n\nlint:\n\tpylint --rcfile=setup.cfg lbry\n\t#mypy --ignore-missing-imports lbry\n\ntest: test-unit test-integration\n\ntest-unit:\n\tpython -m unittest discover tests.unit\n\ntest-unit-coverage:\n\tcoverage run --source=lbry -m unittest discover -vv tests.unit\n\ntest-integration:\n\ttox\n\nidea:\n\tmkdir -p .idea\n\tcp -r scripts/idea/* .idea\n\nelastic-docker:\n\tdocker 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\n"
  },
  {
    "path": "README.md",
    "content": "# <img src=\"https://raw.githubusercontent.com/lbryio/lbry-sdk/master/lbry.png\" alt=\"LBRY\" width=\"48\" height=\"36\" /> LBRY SDK [![build](https://github.com/lbryio/lbry-sdk/actions/workflows/main.yml/badge.svg)](https://github.com/lbryio/lbry-sdk/actions/workflows/main.yml) [![coverage](https://coveralls.io/repos/github/lbryio/lbry-sdk/badge.svg)](https://coveralls.io/github/lbryio/lbry-sdk)\n\nLBRY 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.\n\nLBRY 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:\n\n * Built on Python 3.7 and `asyncio`.\n * 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)).\n * 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)).\n * Protobuf schema for encoding and decoding metadata stored on the blockchain ([lbry.schema](https://github.com/lbryio/lbry-sdk/tree/master/lbry/schema)).\n * Wallet implementation for the LBRY blockchain ([lbry.wallet](https://github.com/lbryio/lbry-sdk/tree/master/lbry/wallet)).\n * 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)). \n\n## Installation\n\nOur [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.\n\n## Usage\n\nRun `lbrynet start` to launch the API server.\n\nBy default, `lbrynet` will provide a JSON-RPC server at `http://localhost:5279`. It is easy to interact with via cURL or sane programming languages.\n\nOur [quickstart guide](https://lbry.tech/playground) provides a simple walkthrough and examples for learning.\n\nWith the daemon running, `lbrynet commands` will show you a list of commands.\n\nThe full API is documented [here](https://lbry.tech/api/sdk).\n\n## Running from source\n\nInstalling from source is also relatively painless. Full instructions are in [INSTALL.md](INSTALL.md)\n\n## Contributing\n\nContributions to this project are welcome, encouraged, and compensated. For more details, please check [this](https://lbry.tech/contribute) link.\n\n## License\n\nThis project is MIT licensed. For the full license, see [LICENSE](LICENSE).\n\n## Security\n\nWe 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.\n\n## Contact\n\nThe primary contact for this project is [@eukreign](mailto:lex@lbry.com).\n\n## Additional information and links\n\nThe documentation for the API can be found [here](https://lbry.tech/api/sdk).\n\nDaemon defaults, ports, and other settings are documented [here](https://lbry.tech/resources/daemon-settings).\n\nSettings 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).\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nWhile we are not at v1.0 yet, only the latest release will be supported.\n\n## Reporting a Vulnerability\n\nSee https://lbry.com/faq/security\n"
  },
  {
    "path": "docker/Dockerfile.dht_node",
    "content": "FROM debian:10-slim\n\nARG user=lbry\nARG projects_dir=/home/$user\nARG db_dir=/database\n\nARG DOCKER_TAG\nARG DOCKER_COMMIT=docker\nENV DOCKER_TAG=$DOCKER_TAG DOCKER_COMMIT=$DOCKER_COMMIT\n\nRUN apt-get update && \\\n    apt-get -y --no-install-recommends install \\\n      wget \\\n      automake libtool \\\n      tar unzip \\\n      build-essential \\\n      pkg-config \\\n      libleveldb-dev \\\n      python3.7 \\\n      python3-dev \\\n      python3-pip \\\n      python3-wheel \\\n      python3-setuptools && \\\n    update-alternatives --install /usr/bin/pip pip /usr/bin/pip3 1 && \\\n    rm -rf /var/lib/apt/lists/*\n\nRUN groupadd -g 999 $user && useradd -m -u 999 -g $user $user\n\nCOPY . $projects_dir\nRUN chown -R $user:$user $projects_dir\nRUN mkdir -p $db_dir\nRUN chown -R $user:$user $db_dir\n\nUSER $user\nWORKDIR $projects_dir\n\nRUN python3 -m pip install -U setuptools pip\nRUN make install\nRUN python3 docker/set_build.py\nRUN rm ~/.cache -rf\nVOLUME $db_dir\nENTRYPOINT [\"python3\", \"scripts/dht_node.py\"]\n\n"
  },
  {
    "path": "docker/Dockerfile.wallet_server",
    "content": "FROM debian:10-slim\n\nARG user=lbry\nARG db_dir=/database\nARG projects_dir=/home/$user\n\nARG DOCKER_TAG\nARG DOCKER_COMMIT=docker\nENV DOCKER_TAG=$DOCKER_TAG DOCKER_COMMIT=$DOCKER_COMMIT\n\nRUN apt-get update && \\\n    apt-get -y --no-install-recommends install \\\n      wget \\\n      tar unzip \\\n      build-essential \\\n      automake libtool \\\n      pkg-config \\\n      libleveldb-dev \\\n      python3.7 \\\n      python3-dev \\\n      python3-pip \\\n      python3-wheel \\\n      python3-cffi \\\n      python3-setuptools && \\\n    update-alternatives --install /usr/bin/pip pip /usr/bin/pip3 1 && \\\n    rm -rf /var/lib/apt/lists/*\n\nRUN groupadd -g 999 $user && useradd -m -u 999 -g $user $user\nRUN mkdir -p $db_dir\nRUN chown -R $user:$user $db_dir\n\nCOPY . $projects_dir\nRUN chown -R $user:$user $projects_dir\n\nUSER $user\nWORKDIR $projects_dir\n\nRUN pip install uvloop\nRUN make install\nRUN python3 docker/set_build.py\nRUN rm ~/.cache -rf\n\n# entry point\nARG host=0.0.0.0\nARG tcp_port=50001\nARG daemon_url=http://lbry:lbry@localhost:9245/\nVOLUME $db_dir\nENV TCP_PORT=$tcp_port\nENV HOST=$host\nENV DAEMON_URL=$daemon_url\nENV DB_DIRECTORY=$db_dir\nENV MAX_SESSIONS=1000000000\nENV MAX_SEND=1000000000000000000\nENV EVENT_LOOP_POLICY=uvloop\nCOPY ./docker/wallet_server_entrypoint.sh /entrypoint.sh\nENTRYPOINT [\"/entrypoint.sh\"]\n"
  },
  {
    "path": "docker/Dockerfile.web",
    "content": "FROM debian:10-slim\n\nARG user=lbry\nARG downloads_dir=/database\nARG projects_dir=/home/$user\n\nARG DOCKER_TAG\nARG DOCKER_COMMIT=docker\nENV DOCKER_TAG=$DOCKER_TAG DOCKER_COMMIT=$DOCKER_COMMIT\n\nRUN apt-get update && \\\n    apt-get -y --no-install-recommends install \\\n      wget \\\n      automake libtool \\\n      tar unzip \\\n      build-essential \\\n      pkg-config \\\n      libleveldb-dev \\\n      python3.7 \\\n      python3-dev \\\n      python3-pip \\\n      python3-wheel \\\n      python3-setuptools && \\\n    update-alternatives --install /usr/bin/pip pip /usr/bin/pip3 1 && \\\n    rm -rf /var/lib/apt/lists/*\n\nRUN groupadd -g 999 $user && useradd -m -u 999 -g $user $user\nRUN mkdir -p $downloads_dir\nRUN chown -R $user:$user $downloads_dir\n\nCOPY . $projects_dir\nRUN chown -R $user:$user $projects_dir\n\nUSER $user\nWORKDIR $projects_dir\n\nRUN pip install uvloop\nRUN make install\nRUN python3 docker/set_build.py\nRUN rm ~/.cache -rf\n\n# entry point\nVOLUME $downloads_dir\nCOPY ./docker/webconf.yaml /webconf.yaml\nENTRYPOINT [\"/home/lbry/.local/bin/lbrynet\", \"start\", \"--config=/webconf.yaml\"]\n"
  },
  {
    "path": "docker/README.md",
    "content": "### How to run with docker-compose\n1. Edit config file and after that fix permissions with\n```\nsudo chown -R 999:999 webconf.yaml\n```\n2. Start SDK with\n```\ndocker-compose up -d\n```\n"
  },
  {
    "path": "docker/docker-compose-wallet-server.yml",
    "content": "version: \"3\"\n\nvolumes:\n  wallet_server:\n  es01:\n\nservices:\n  wallet_server:\n    depends_on: \n      - es01\n    image: lbry/wallet-server:${WALLET_SERVER_TAG:-latest-release}\n    restart: always\n    network_mode: host\n    ports:\n      - \"50001:50001\" # rpc port\n      - \"2112:2112\"   # uncomment to enable prometheus\n    volumes:\n      - \"wallet_server:/database\"\n    environment:\n      - DAEMON_URL=http://lbry:lbry@127.0.0.1:9245\n      - MAX_QUERY_WORKERS=4\n      - CACHE_MB=1024\n      - CACHE_ALL_TX_HASHES=\n      - CACHE_ALL_CLAIM_TXOS=\n      - MAX_SEND=1000000000000000000\n      - MAX_RECEIVE=1000000000000000000\n      - MAX_SESSIONS=100000\n      - HOST=0.0.0.0\n      - TCP_PORT=50001\n      - PROMETHEUS_PORT=2112\n      - FILTERING_CHANNEL_IDS=770bd7ecba84fd2f7607fb15aedd2b172c2e153f 95e5db68a3101df19763f3a5182e4b12ba393ee8\n      - BLOCKING_CHANNEL_IDS=dd687b357950f6f271999971f43c785e8067c3a9 06871aa438032244202840ec59a469b303257cad b4a2528f436eca1bf3bf3e10ff3f98c57bd6c4c6\n  es01:\n    image: docker.elastic.co/elasticsearch/elasticsearch:7.11.0\n    container_name: es01\n    environment:\n      - node.name=es01\n      - discovery.type=single-node\n      - indices.query.bool.max_clause_count=8192\n      - bootstrap.memory_lock=true\n      - \"ES_JAVA_OPTS=-Xms4g -Xmx4g\"  # no more than 32, remember to disable swap\n    ulimits:\n      memlock:\n        soft: -1\n        hard: -1\n    volumes:\n      - es01:/usr/share/elasticsearch/data\n    ports:\n      - 127.0.0.1:9200:9200\n"
  },
  {
    "path": "docker/docker-compose.yml",
    "content": "version: '3'\nservices:\n    websdk:\n        image: vshyba/websdk\n        ports:\n            - '5279:5279'\n            - '5280:5280'\n        volumes:\n            - ./webconf.yaml:/webconf.yaml\n"
  },
  {
    "path": "docker/hooks/build",
    "content": "#!/bin/bash\n\nDIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" >/dev/null 2>&1 && pwd )\"\ncd \"$DIR/../..\" ## make sure we're in the right place. Docker Hub screws this up sometimes\necho \"docker build dir: $(pwd)\"\n\ndocker build --build-arg DOCKER_TAG=$DOCKER_TAG --build-arg DOCKER_COMMIT=$SOURCE_COMMIT -f $DOCKERFILE_PATH -t $IMAGE_NAME .\n"
  },
  {
    "path": "docker/install_choco.ps1",
    "content": "# requires powershell and .NET 4+. see https://chocolatey.org/install for more info.\n\n$chocoVersion = powershell choco -v\nif(-not($chocoVersion)){\n    Write-Output \"Chocolatey is not installed, installing now\"\n    Write-Output \"IF YOU KEEP GETTING THIS MESSAGE ON EVERY BUILD, TRY RESTARTING THE GITLAB RUNNER SO IT GETS CHOCO INTO IT'S ENV\"\n    Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))\n}\nelse{\n    Write-Output \"Chocolatey version $chocoVersion is already installed\"\n}"
  },
  {
    "path": "docker/set_build.py",
    "content": "import sys\nimport os\nimport re\nimport logging\nimport lbry.build_info as build_info_mod\n\nlog = logging.getLogger()\nlog.addHandler(logging.StreamHandler())\nlog.setLevel(logging.DEBUG)\n\n\ndef _check_and_set(d: dict, key: str, value: str):\n    try:\n        d[key]\n    except KeyError:\n        raise Exception(f\"{key} var does not exist in {build_info_mod.__file__}\")\n    d[key] = value\n\n\ndef main():\n    build_info = {item: build_info_mod.__dict__[item] for item in dir(build_info_mod) if not item.startswith(\"__\")}\n\n    commit_hash = os.getenv('DOCKER_COMMIT', os.getenv('GITHUB_SHA'))\n    if commit_hash is None:\n        raise ValueError(\"Commit hash not found in env vars\")\n    _check_and_set(build_info, \"COMMIT_HASH\", commit_hash[:6])\n\n    docker_tag = os.getenv('DOCKER_TAG')\n    if docker_tag:\n        _check_and_set(build_info, \"DOCKER_TAG\", docker_tag)\n        _check_and_set(build_info, \"BUILD\", \"docker\")\n    else:\n        if re.match(r'refs/tags/v\\d+\\.\\d+\\.\\d+$', str(os.getenv('GITHUB_REF'))):\n            _check_and_set(build_info, \"BUILD\", \"release\")\n        else:\n            _check_and_set(build_info, \"BUILD\", \"qa\")\n\n    log.debug(\"build info: %s\", \", \".join([f\"{k}={v}\" for k, v in build_info.items()]))\n    with open(build_info_mod.__file__, 'w') as f:\n        f.write(\"\\n\".join([f\"{k} = \\\"{v}\\\"\" for k, v in build_info.items()]) + \"\\n\")\n\n\nif __name__ == '__main__':\n    sys.exit(main())\n"
  },
  {
    "path": "docker/wallet_server_entrypoint.sh",
    "content": "#!/bin/bash\n\n# entrypoint for wallet server Docker image\n\nset -euo pipefail\n\nSNAPSHOT_URL=\"${SNAPSHOT_URL:-}\" #off by default. latest snapshot at https://lbry.com/snapshot/wallet\n\nif [[ -n \"$SNAPSHOT_URL\" ]] && [[ ! -f /database/lbry-leveldb ]]; then\n  files=\"$(ls)\"\n  echo \"Downloading wallet snapshot from $SNAPSHOT_URL\"\n  wget --no-verbose --trust-server-names --content-disposition \"$SNAPSHOT_URL\"\n  echo \"Extracting snapshot...\"\n  filename=\"$(grep -vf <(echo \"$files\") <(ls))\" # finds the file that was not there before\n  case \"$filename\" in\n    *.tgz|*.tar.gz|*.tar.bz2 )  tar xvf \"$filename\" --directory /database ;;\n    *.zip ) unzip \"$filename\" -d /database ;;\n    * ) echo \"Don't know how to extract ${filename}. SNAPSHOT COULD NOT BE LOADED\" && exit 1 ;;\n  esac\n  rm \"$filename\"\nfi\n\n/home/lbry/.local/bin/lbry-hub-elastic-sync\necho 'starting server'\n/home/lbry/.local/bin/lbry-hub \"$@\"\n"
  },
  {
    "path": "docker/webconf.yaml",
    "content": "allowed_origin: \"*\"\nmax_key_fee: \"0.0 USD\"\nsave_files: false\nsave_blobs: false\nstreaming_server: \"0.0.0.0:5280\"\napi: \"0.0.0.0:5279\"\ndata_dir: /tmp\ndownload_dir: /tmp\nwallet_dir: /tmp\n"
  },
  {
    "path": "docs/api.json",
    "content": "{\n    \"main\": {\n        \"doc\": \"Ungrouped commands.\",\n        \"commands\": [\n            {\n                \"name\": \"ffmpeg_find\",\n                \"description\": \"Get ffmpeg installation information\",\n                \"arguments\": [],\n                \"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    }\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"get\",\n                \"description\": \"Download stream from a LBRY name.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"uri\",\n                        \"type\": \"str\",\n                        \"description\": \"uri of the content to download\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"file_name\",\n                        \"type\": \"str\",\n                        \"description\": \"specified name for the downloaded file, overrides the stream file name\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"download_directory\",\n                        \"type\": \"str\",\n                        \"description\": \"full path to the directory to download into\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"timeout\",\n                        \"type\": \"int\",\n                        \"description\": \"download timeout in number of seconds\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"save_file\",\n                        \"type\": \"bool\",\n                        \"description\": \"save the file to the downloads directory\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"wallet to check for claim purchase receipts\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"Get a file\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"get\\\", \\\"params\\\": {\\\"uri\\\": \\\"astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\\\"}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet get astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"get\\\", \\\"params\\\": {\\\"uri\\\": \\\"astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\\\"}}).json()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"publish\",\n                \"description\": \"Create or replace a stream claim at a given name (use 'stream create/update' for more control).\",\n                \"arguments\": [\n                    {\n                        \"name\": \"name\",\n                        \"type\": \"str\",\n                        \"description\": \"name of the content (can only consist of a-z A-Z 0-9 and -(dash))\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"bid\",\n                        \"type\": \"decimal\",\n                        \"description\": \"amount to back the claim\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"file_path\",\n                        \"type\": \"str\",\n                        \"description\": \"path to file to be associated with name.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"file_name\",\n                        \"type\": \"str\",\n                        \"description\": \"name of file to be associated with stream.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"file_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"hash of file to be associated with stream.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"validate_file\",\n                        \"type\": \"bool\",\n                        \"description\": \"validate that the video container and encodings match common web browser support or that optimization succeeds if specified. FFmpeg is required\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"optimize_file\",\n                        \"type\": \"bool\",\n                        \"description\": \"transcode the video & audio if necessary to ensure common web browser support. FFmpeg is required\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"fee_currency\",\n                        \"type\": \"string\",\n                        \"description\": \"specify fee currency\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"fee_amount\",\n                        \"type\": \"decimal\",\n                        \"description\": \"content download fee\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"fee_address\",\n                        \"type\": \"str\",\n                        \"description\": \"address where to send fee payments, will use value from --claim_address if not provided\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"title\",\n                        \"type\": \"str\",\n                        \"description\": \"title of the publication\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"description\",\n                        \"type\": \"str\",\n                        \"description\": \"description of the publication\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"author\",\n                        \"type\": \"str\",\n                        \"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\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"tags\",\n                        \"type\": \"list\",\n                        \"description\": \"add content tags\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"languages\",\n                        \"type\": \"list\",\n                        \"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`\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"locations\",\n                        \"type\": \"list\",\n                        \"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'}\\\"\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"license\",\n                        \"type\": \"str\",\n                        \"description\": \"publication license\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"license_url\",\n                        \"type\": \"str\",\n                        \"description\": \"publication license url\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"thumbnail_url\",\n                        \"type\": \"str\",\n                        \"description\": \"thumbnail url\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"release_time\",\n                        \"type\": \"int\",\n                        \"description\": \"original public release of content, seconds since UNIX epoch\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"width\",\n                        \"type\": \"int\",\n                        \"description\": \"image/video width, automatically calculated from media file\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"height\",\n                        \"type\": \"int\",\n                        \"description\": \"image/video height, automatically calculated from media file\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"duration\",\n                        \"type\": \"int\",\n                        \"description\": \"audio/video duration in seconds, automatically calculated\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"sd_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"sd_hash of stream\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_id\",\n                        \"type\": \"str\",\n                        \"description\": \"claim id of the publisher channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_name\",\n                        \"type\": \"str\",\n                        \"description\": \"name of publisher channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"one or more account ids for accounts to look in for channel certificates, defaults to all accounts.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"account to use for holding the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"funding_account_ids\",\n                        \"type\": \"list\",\n                        \"description\": \"ids of accounts to fund this transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_address\",\n                        \"type\": \"str\",\n                        \"description\": \"address where the claim is sent to, if not specified it will be determined automatically from the account\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"preview\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not broadcast the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"blocking\",\n                        \"type\": \"bool\",\n                        \"description\": \"wait until transaction is in mempool\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"Publish a file\",\n                        \"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/\",\n                        \"lbrynet\": \"lbrynet publish a-new-stream --bid=1.0 --file_path=/var/folders/46/44w2zhrx16b8gsvff9dxtr640000gq/T/tmp1wt4ndjd\",\n                        \"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()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"resolve\",\n                \"description\": \"Get the claim that a URL refers to.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"urls\",\n                        \"type\": \"str, list\",\n                        \"description\": \"one or more urls to resolve\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"wallet to check for claim purchase receipts\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"new_sdk_server\",\n                        \"type\": \"str\",\n                        \"description\": \"URL of the new SDK server (EXPERIMENTAL)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"include_purchase_receipt\",\n                        \"type\": \"bool\",\n                        \"description\": \"lookup and include a receipt if this wallet has purchased the claim being resolved\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"include_is_my_output\",\n                        \"type\": \"bool\",\n                        \"description\": \"lookup and include a boolean indicating if claim being resolved is yours\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"include_sent_supports\",\n                        \"type\": \"bool\",\n                        \"description\": \"lookup and sum the total amount of supports you've made to this claim\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"include_sent_tips\",\n                        \"type\": \"bool\",\n                        \"description\": \"lookup and sum the total amount of tips you've made to this claim (only makes sense when claim is not yours)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"include_received_tips\",\n                        \"type\": \"bool\",\n                        \"description\": \"lookup and sum the total amount of tips you've received to this claim (only makes sense when claim is yours)\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"Dictionary of results, keyed by url\\n    '<url>': {\\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    }\",\n                \"examples\": [\n                    {\n                        \"title\": \"Resolve a claim\",\n                        \"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/\",\n                        \"lbrynet\": \"lbrynet resolve astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n                        \"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()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"routing_table_get\",\n                \"description\": \"Get DHT routing information\",\n                \"arguments\": [],\n                \"returns\": \"(dict) dictionary containing routing and peer information\\n    {\\n        \\\"buckets\\\": {\\n            <bucket index>: [\\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    }\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"status\",\n                \"description\": \"Get daemon status\",\n                \"arguments\": [],\n                \"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                    <source ip and tcp port>: (int) bytes per second received,\\n                },\\n                'outgoing_bps': {\\n                    <destination ip and tcp port>: (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                <TCP | UDP>: (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    }\",\n                \"examples\": [\n                    {\n                        \"title\": \"Get status\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"status\\\", \\\"params\\\": {}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet status\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"status\\\", \\\"params\\\": {}}).json()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"stop\",\n                \"description\": \"Stop lbrynet API server.\",\n                \"arguments\": [],\n                \"returns\": \"(string) Shutdown message\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"version\",\n                \"description\": \"Get lbrynet API server version information\",\n                \"arguments\": [],\n                \"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    }\",\n                \"examples\": [\n                    {\n                        \"title\": \"Get version\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"version\\\", \\\"params\\\": {}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet version\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"version\\\", \\\"params\\\": {}}).json()\",\n                        \"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}\"\n                    }\n                ]\n            }\n        ]\n    },\n    \"account\": {\n        \"doc\": \"Create, modify and inspect wallet accounts.\",\n        \"commands\": [\n            {\n                \"name\": \"account_add\",\n                \"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.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"account_name\",\n                        \"type\": \"str\",\n                        \"description\": \"name of the account to add\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"seed\",\n                        \"type\": \"str\",\n                        \"description\": \"seed to generate new account from\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"private_key\",\n                        \"type\": \"str\",\n                        \"description\": \"private key for new account\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"public_key\",\n                        \"type\": \"str\",\n                        \"description\": \"public key for new account\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"single_key\",\n                        \"type\": \"bool\",\n                        \"description\": \"create single key account, default is multi-key\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"Add an account from seed\",\n                        \"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/\",\n                        \"lbrynet\": \"lbrynet account add \\\"new account\\\" --seed=\\\"miss ready crop oval canyon such sing powder figure math noodle style\\\"\",\n                        \"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()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"account_balance\",\n                \"description\": \"Return the balance of an account\",\n                \"arguments\": [\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"If provided only the balance for this account will be given. Otherwise default account.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"balance for specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"confirmations\",\n                        \"type\": \"int\",\n                        \"description\": \"Only include transactions with this many confirmed blocks.\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(decimal) amount of lbry credits in wallet\",\n                \"examples\": [\n                    {\n                        \"title\": \"Get default account balance\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"account_balance\\\", \\\"params\\\": {}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet account balance\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"account_balance\\\", \\\"params\\\": {}}).json()\",\n                        \"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}\"\n                    },\n                    {\n                        \"title\": \"Get balance for specific account by id\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"account_balance\\\", \\\"params\\\": {\\\"account_id\\\": \\\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\\\"}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet account balance \\\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\\\"\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"account_balance\\\", \\\"params\\\": {\\\"account_id\\\": \\\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\\\"}}).json()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"account_create\",\n                \"description\": \"Create a new account. Specify --single_key if you want to use\\nthe same address for all transactions (not recommended).\",\n                \"arguments\": [\n                    {\n                        \"name\": \"account_name\",\n                        \"type\": \"str\",\n                        \"description\": \"name of the account to create\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"single_key\",\n                        \"type\": \"bool\",\n                        \"description\": \"create single key account, default is multi-key\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"Create an account\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"account_create\\\", \\\"params\\\": {\\\"account_name\\\": \\\"generated account\\\", \\\"single_key\\\": false}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet account create \\\"generated account\\\"\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"account_create\\\", \\\"params\\\": {\\\"account_name\\\": \\\"generated account\\\", \\\"single_key\\\": false}}).json()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"account_deposit\",\n                \"description\": \"Spend a time locked transaction into your account.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"txid\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"nout\",\n                        \"type\": \"int\",\n                        \"description\": \"output number in the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"redeem_script\",\n                        \"type\": \"str\",\n                        \"description\": \"redeem script for output\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"private_key\",\n                        \"type\": \"str\",\n                        \"description\": \"private key to sign transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"to_account\",\n                        \"type\": \"str\",\n                        \"description\": \"deposit to this account\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"limit operation to specific wallet.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"preview\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not broadcast the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"blocking\",\n                        \"type\": \"bool\",\n                        \"description\": \"wait until tx has synced\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"account_fund\",\n                \"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).\",\n                \"arguments\": [\n                    {\n                        \"name\": \"to_account\",\n                        \"type\": \"str\",\n                        \"description\": \"send to this account\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"from_account\",\n                        \"type\": \"str\",\n                        \"description\": \"spend from this account\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"amount\",\n                        \"type\": \"decimal\",\n                        \"description\": \"the amount to transfer lbc\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"everything\",\n                        \"type\": \"bool\",\n                        \"description\": \"transfer everything (excluding claims), default: false.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"outputs\",\n                        \"type\": \"int\",\n                        \"description\": \"split payment across many outputs, default: 1.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"limit operation to specific wallet.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"broadcast\",\n                        \"type\": \"bool\",\n                        \"description\": \"actually broadcast the transaction, default: false.\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"Transfer 2 LBC from default account to specific account\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"account_fund\\\", \\\"params\\\": {\\\"to_account\\\": \\\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\\\", \\\"amount\\\": \\\"2.0\\\", \\\"everything\\\": false, \\\"broadcast\\\": true}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet account fund --to_account=\\\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\\\" --amount=2.0 --broadcast\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"account_fund\\\", \\\"params\\\": {\\\"to_account\\\": \\\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\\\", \\\"amount\\\": \\\"2.0\\\", \\\"everything\\\": false, \\\"broadcast\\\": true}}).json()\",\n                        \"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}\"\n                    },\n                    {\n                        \"title\": \"Spread LBC between multiple addresses\",\n                        \"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/\",\n                        \"lbrynet\": \"lbrynet account fund --to_account=\\\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\\\" --from_account=\\\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\\\" --amount=1.5 --outputs=2 --broadcast\",\n                        \"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()\",\n                        \"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}\"\n                    },\n                    {\n                        \"title\": \"Transfer all LBC to a specified account\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"account_fund\\\", \\\"params\\\": {\\\"from_account\\\": \\\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\\\", \\\"everything\\\": true, \\\"broadcast\\\": true}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet account fund --from_account=\\\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\\\" --everything --broadcast\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"account_fund\\\", \\\"params\\\": {\\\"from_account\\\": \\\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\\\", \\\"everything\\\": true, \\\"broadcast\\\": true}}).json()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"account_list\",\n                \"description\": \"List details of all of the accounts or a specific account.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"If provided only the balance for this account will be given\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"accounts in specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"confirmations\",\n                        \"type\": \"int\",\n                        \"description\": \"required confirmations (default: 0)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"include_claims\",\n                        \"type\": \"bool\",\n                        \"description\": \"include claims, requires than a LBC account is specified (default: false)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"show_seed\",\n                        \"type\": \"bool\",\n                        \"description\": \"show the seed for the account\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page\",\n                        \"type\": \"int\",\n                        \"description\": \"page to return during paginating\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page_size\",\n                        \"type\": \"int\",\n                        \"description\": \"number of items on page during pagination\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"List your accounts\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"account_list\\\", \\\"params\\\": {\\\"include_claims\\\": false, \\\"show_seed\\\": false}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet account list\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"account_list\\\", \\\"params\\\": {\\\"include_claims\\\": false, \\\"show_seed\\\": false}}).json()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"account_max_address_gap\",\n                \"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.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"account for which to get max gaps\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(map) maximum gap for change and receiving addresses\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"account_remove\",\n                \"description\": \"Remove an existing account.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to remove\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"Remove an account\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"account_remove\\\", \\\"params\\\": {\\\"account_id\\\": \\\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\\\"}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet account remove mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"account_remove\\\", \\\"params\\\": {\\\"account_id\\\": \\\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\\\"}}).json()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"account_send\",\n                \"description\": \"Send the same number of credits to multiple addresses from a specific account (or default account).\",\n                \"arguments\": [\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"account to fund the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"preview\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not broadcast the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"blocking\",\n                        \"type\": \"bool\",\n                        \"description\": \"wait until tx has synced\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"account_set\",\n                \"description\": \"Change various settings on an account.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to change\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"default\",\n                        \"type\": \"bool\",\n                        \"description\": \"make this account the default\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"new_name\",\n                        \"type\": \"str\",\n                        \"description\": \"new name for the account\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"receiving_gap\",\n                        \"type\": \"int\",\n                        \"description\": \"set the gap for receiving addresses\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"receiving_max_uses\",\n                        \"type\": \"int\",\n                        \"description\": \"set the maximum number of times to use a receiving address\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"change_gap\",\n                        \"type\": \"int\",\n                        \"description\": \"set the gap for change addresses\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"change_max_uses\",\n                        \"type\": \"int\",\n                        \"description\": \"set the maximum number of times to use a change address\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"Modify maximum number of times a change address can be reused\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"account_set\\\", \\\"params\\\": {\\\"account_id\\\": \\\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\\\", \\\"default\\\": false, \\\"change_max_uses\\\": 10}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet account set mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn --change_max_uses=10\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"account_set\\\", \\\"params\\\": {\\\"account_id\\\": \\\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\\\", \\\"default\\\": false, \\\"change_max_uses\\\": 10}}).json()\",\n                        \"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}\"\n                    }\n                ]\n            }\n        ]\n    },\n    \"address\": {\n        \"doc\": \"List, generate and verify addresses.\",\n        \"commands\": [\n            {\n                \"name\": \"address_is_mine\",\n                \"description\": \"Checks if an address is associated with the current wallet.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"address\",\n                        \"type\": \"str\",\n                        \"description\": \"address to check\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to use\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(bool) true, if address is associated with current wallet\",\n                \"examples\": [\n                    {\n                        \"title\": \"Check if address is mine\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"address_is_mine\\\", \\\"params\\\": {\\\"address\\\": \\\"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\\\"}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet address is_mine muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"address_is_mine\\\", \\\"params\\\": {\\\"address\\\": \\\"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\\\"}}).json()\",\n                        \"output\": \"{\\n  \\\"jsonrpc\\\": \\\"2.0\\\",\\n  \\\"result\\\": true\\n}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"address_list\",\n                \"description\": \"List account addresses or details of single address.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"address\",\n                        \"type\": \"str\",\n                        \"description\": \"just show details for single address\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to use\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page\",\n                        \"type\": \"int\",\n                        \"description\": \"page to return during paginating\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page_size\",\n                        \"type\": \"int\",\n                        \"description\": \"number of items on page during pagination\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"List addresses in default account\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"address_list\\\", \\\"params\\\": {}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet address list\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"address_list\\\", \\\"params\\\": {}}).json()\",\n                        \"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}\"\n                    },\n                    {\n                        \"title\": \"List addresses in specified account\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"address_list\\\", \\\"params\\\": {\\\"account_id\\\": \\\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\\\"}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet address list --account_id=\\\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\\\"\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"address_list\\\", \\\"params\\\": {\\\"account_id\\\": \\\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\\\"}}).json()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"address_unused\",\n                \"description\": \"Return an address containing no balance, will create\\na new address if there is none.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to use\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"            \\\"an address in base58\\\"\",\n                \"examples\": [\n                    {\n                        \"title\": \"Get an unused address\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"address_unused\\\", \\\"params\\\": {}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet address unused\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"address_unused\\\", \\\"params\\\": {}}).json()\",\n                        \"output\": \"{\\n  \\\"jsonrpc\\\": \\\"2.0\\\",\\n  \\\"result\\\": \\\"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\\\"\\n}\"\n                    }\n                ]\n            }\n        ]\n    },\n    \"blob\": {\n        \"doc\": \"Blob management.\",\n        \"commands\": [\n            {\n                \"name\": \"blob_announce\",\n                \"description\": \"Announce blobs to the DHT\",\n                \"arguments\": [\n                    {\n                        \"name\": \"blob_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"announce a blob, specified by blob_hash\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"stream_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"announce all blobs associated with stream_hash\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"sd_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"announce all blobs associated with sd_hash and the sd_hash itself\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(bool) true if successful\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"blob_clean\",\n                \"description\": \"Deletes blobs to cleanup disk space\",\n                \"arguments\": [],\n                \"returns\": \"(bool) true if successful\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"blob_delete\",\n                \"description\": \"Delete a blob\",\n                \"arguments\": [\n                    {\n                        \"name\": \"blob_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"blob hash of the blob to delete\",\n                        \"is_required\": true\n                    }\n                ],\n                \"returns\": \"(str) Success/fail message\",\n                \"examples\": [\n                    {\n                        \"title\": \"Delete a blob\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"blob_delete\\\", \\\"params\\\": {\\\"blob_hash\\\": \\\"d221fe243afed69b84e6cbc32448260a187449c5123bfa89d33af05af2c8f195a7bbac2bfe02d4a2ba472af217af3996\\\"}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet blob delete d221fe243afed69b84e6cbc32448260a187449c5123bfa89d33af05af2c8f195a7bbac2bfe02d4a2ba472af217af3996\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"blob_delete\\\", \\\"params\\\": {\\\"blob_hash\\\": \\\"d221fe243afed69b84e6cbc32448260a187449c5123bfa89d33af05af2c8f195a7bbac2bfe02d4a2ba472af217af3996\\\"}}).json()\",\n                        \"output\": \"{\\n  \\\"jsonrpc\\\": \\\"2.0\\\",\\n  \\\"result\\\": \\\"Deleted d221fe243afed69b84e6cbc32448260a187449c5123bfa89d33af05af2c8f195a7bbac2bfe02d4a2ba472af217af3996\\\"\\n}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"blob_get\",\n                \"description\": \"Download and return a blob\",\n                \"arguments\": [\n                    {\n                        \"name\": \"blob_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"blob hash of the blob to get\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"timeout\",\n                        \"type\": \"int\",\n                        \"description\": \"timeout in number of seconds\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(str) Success/Fail message or (dict) decoded data\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"blob_list\",\n                \"description\": \"Returns blob hashes. If not given filters, returns all blobs known by the blob manager\",\n                \"arguments\": [\n                    {\n                        \"name\": \"needed\",\n                        \"type\": \"bool\",\n                        \"description\": \"only return needed blobs\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"finished\",\n                        \"type\": \"bool\",\n                        \"description\": \"only return finished blobs\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"uri\",\n                        \"type\": \"str\",\n                        \"description\": \"filter blobs by stream in a uri\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"stream_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"filter blobs by stream hash\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"sd_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"filter blobs in a stream by sd hash, ie the hash of the stream descriptor blob for a stream that has been downloaded\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page\",\n                        \"type\": \"int\",\n                        \"description\": \"page to return during paginating\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page_size\",\n                        \"type\": \"int\",\n                        \"description\": \"number of items on page during pagination\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(list) List of blob hashes\",\n                \"examples\": [\n                    {\n                        \"title\": \"List your local blobs\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"blob_list\\\", \\\"params\\\": {\\\"needed\\\": false, \\\"finished\\\": false}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet blob list\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"blob_list\\\", \\\"params\\\": {\\\"needed\\\": false, \\\"finished\\\": false}}).json()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"blob_reflect\",\n                \"description\": \"Reflects specified blobs\",\n                \"arguments\": [\n                    {\n                        \"name\": \"reflector_server\",\n                        \"type\": \"str\",\n                        \"description\": \"reflector address\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(list) reflected blob hashes\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"blob_reflect_all\",\n                \"description\": \"Reflects all saved blobs\",\n                \"arguments\": [],\n                \"returns\": \"(bool) true if successful\",\n                \"examples\": []\n            }\n        ]\n    },\n    \"channel\": {\n        \"doc\": \"Create, update, abandon and list your channel claims.\",\n        \"commands\": [\n            {\n                \"name\": \"channel_abandon\",\n                \"description\": \"Abandon one of my channel claims.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str\",\n                        \"description\": \"claim_id of the claim to abandon\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"txid\",\n                        \"type\": \"str\",\n                        \"description\": \"txid of the claim to abandon\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"nout\",\n                        \"type\": \"int\",\n                        \"description\": \"nout of the claim to abandon\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to use\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"preview\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not broadcast the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"blocking\",\n                        \"type\": \"bool\",\n                        \"description\": \"wait until abandon is in mempool\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"Abandon a channel claim\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"channel_abandon\\\", \\\"params\\\": {\\\"claim_id\\\": \\\"595c2e2f0c1f59188628fab7503692b0145779a2\\\", \\\"preview\\\": false, \\\"blocking\\\": false}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet channel abandon 595c2e2f0c1f59188628fab7503692b0145779a2\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"channel_abandon\\\", \\\"params\\\": {\\\"claim_id\\\": \\\"595c2e2f0c1f59188628fab7503692b0145779a2\\\", \\\"preview\\\": false, \\\"blocking\\\": false}}).json()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"channel_create\",\n                \"description\": \"Create a new channel by generating a channel private key and establishing an '@' prefixed claim.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"name\",\n                        \"type\": \"str\",\n                        \"description\": \"name of the channel prefixed with '@'\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"bid\",\n                        \"type\": \"decimal\",\n                        \"description\": \"amount to back the claim\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"allow_duplicate_name\",\n                        \"type\": \"bool\",\n                        \"description\": \"create new channel even if one already exists with given name. default: false.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"title\",\n                        \"type\": \"str\",\n                        \"description\": \"title of the publication\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"description\",\n                        \"type\": \"str\",\n                        \"description\": \"description of the publication\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"email\",\n                        \"type\": \"str\",\n                        \"description\": \"email of channel owner\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"website_url\",\n                        \"type\": \"str\",\n                        \"description\": \"website url\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"featured\",\n                        \"type\": \"list\",\n                        \"description\": \"claim_ids of featured content in channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"tags\",\n                        \"type\": \"list\",\n                        \"description\": \"content tags\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"languages\",\n                        \"type\": \"list\",\n                        \"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`\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"locations\",\n                        \"type\": \"list\",\n                        \"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'}\\\"\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"thumbnail_url\",\n                        \"type\": \"str\",\n                        \"description\": \"thumbnail url\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"cover_url\",\n                        \"type\": \"str\",\n                        \"description\": \"url of cover image\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"account to use for holding the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"funding_account_ids\",\n                        \"type\": \"list\",\n                        \"description\": \"ids of accounts to fund this transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_address\",\n                        \"type\": \"str\",\n                        \"description\": \"address where the channel is sent to, if not specified it will be determined automatically from the account\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"preview\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not broadcast the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"blocking\",\n                        \"type\": \"bool\",\n                        \"description\": \"wait until transaction is in mempool\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"Create a channel claim without metadata\",\n                        \"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/\",\n                        \"lbrynet\": \"lbrynet channel create @channel 1.0\",\n                        \"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()\",\n                        \"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}\"\n                    },\n                    {\n                        \"title\": \"Create a channel claim with all metadata\",\n                        \"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/\",\n                        \"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\\\"\",\n                        \"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()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"channel_export\",\n                \"description\": \"Export channel private key.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"channel_id\",\n                        \"type\": \"str\",\n                        \"description\": \"claim id of channel to export\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"channel_name\",\n                        \"type\": \"str\",\n                        \"description\": \"name of channel to export\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"one or more account ids for accounts to look in for channels, defaults to all accounts.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(str) serialized channel private key\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"channel_import\",\n                \"description\": \"Import serialized channel private key (to allow signing new streams to the channel)\",\n                \"arguments\": [\n                    {\n                        \"name\": \"channel_data\",\n                        \"type\": \"str\",\n                        \"description\": \"serialized channel, as exported by channel export\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"import into specific wallet\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(dict) Result dictionary\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"channel_list\",\n                \"description\": \"List my channel claims.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"name\",\n                        \"type\": \"str or list\",\n                        \"description\": \"channel name\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str or list\",\n                        \"description\": \"channel id\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_spent\",\n                        \"type\": \"bool\",\n                        \"description\": \"shows previous channel updates and abandons\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to use\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict results to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page\",\n                        \"type\": \"int\",\n                        \"description\": \"page to return during paginating\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page_size\",\n                        \"type\": \"int\",\n                        \"description\": \"number of items on page during pagination\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"resolve\",\n                        \"type\": \"bool\",\n                        \"description\": \"resolves each channel to provide additional metadata\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"no_totals\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not calculate the total number of pages and items in result set (significant performance boost)\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"List your channel claims\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"channel_list\\\", \\\"params\\\": {\\\"name\\\": [], \\\"claim_id\\\": [], \\\"is_spent\\\": false, \\\"resolve\\\": false, \\\"no_totals\\\": false}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet channel list\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"channel_list\\\", \\\"params\\\": {\\\"name\\\": [], \\\"claim_id\\\": [], \\\"is_spent\\\": false, \\\"resolve\\\": false, \\\"no_totals\\\": false}}).json()\",\n                        \"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}\"\n                    },\n                    {\n                        \"title\": \"Paginate your channel claims\",\n                        \"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/\",\n                        \"lbrynet\": \"lbrynet channel list --page=1 --page_size=20\",\n                        \"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()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"channel_sign\",\n                \"description\": \"Signs data using the specified channel signing key.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"channel_name\",\n                        \"type\": \"str\",\n                        \"description\": \"name of channel used to sign (or use channel id)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_id\",\n                        \"type\": \"str\",\n                        \"description\": \"claim id of channel used to sign (or use channel name)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"hexdata\",\n                        \"type\": \"str\",\n                        \"description\": \"data to sign, encoded as hexadecimal\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"one or more account ids for accounts to look in for channel certificates, defaults to all accounts.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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    }\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"channel_update\",\n                \"description\": \"Update an existing channel claim.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str\",\n                        \"description\": \"claim_id of the channel to update\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"bid\",\n                        \"type\": \"decimal\",\n                        \"description\": \"amount to back the claim\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"title\",\n                        \"type\": \"str\",\n                        \"description\": \"title of the publication\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"description\",\n                        \"type\": \"str\",\n                        \"description\": \"description of the publication\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"email\",\n                        \"type\": \"str\",\n                        \"description\": \"email of channel owner\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"website_url\",\n                        \"type\": \"str\",\n                        \"description\": \"website url\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"featured\",\n                        \"type\": \"list\",\n                        \"description\": \"claim_ids of featured content in channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"clear_featured\",\n                        \"type\": \"bool\",\n                        \"description\": \"clear existing featured content (prior to adding new ones)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"tags\",\n                        \"type\": \"list\",\n                        \"description\": \"add content tags\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"clear_tags\",\n                        \"type\": \"bool\",\n                        \"description\": \"clear existing tags (prior to adding new ones)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"languages\",\n                        \"type\": \"list\",\n                        \"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`\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"clear_languages\",\n                        \"type\": \"bool\",\n                        \"description\": \"clear existing languages (prior to adding new ones)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"locations\",\n                        \"type\": \"list\",\n                        \"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'}\\\"\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"clear_locations\",\n                        \"type\": \"bool\",\n                        \"description\": \"clear existing locations (prior to adding new ones)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"thumbnail_url\",\n                        \"type\": \"str\",\n                        \"description\": \"thumbnail url\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"cover_url\",\n                        \"type\": \"str\",\n                        \"description\": \"url of cover image\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"account in which to look for channel (default: all)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"funding_account_ids\",\n                        \"type\": \"list\",\n                        \"description\": \"ids of accounts to fund this transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_address\",\n                        \"type\": \"str\",\n                        \"description\": \"address where the channel is sent\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"new_signing_key\",\n                        \"type\": \"bool\",\n                        \"description\": \"generate a new signing key, will invalidate all previous publishes\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"preview\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not broadcast the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"blocking\",\n                        \"type\": \"bool\",\n                        \"description\": \"wait until transaction is in mempool\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"replace\",\n                        \"type\": \"bool\",\n                        \"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\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"Update a channel claim\",\n                        \"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/\",\n                        \"lbrynet\": \"lbrynet channel update 595c2e2f0c1f59188628fab7503692b0145779a2 --title=\\\"New Channel\\\"\",\n                        \"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()\",\n                        \"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}\"\n                    }\n                ]\n            }\n        ]\n    },\n    \"claim\": {\n        \"doc\": \"List and search all types of claims.\",\n        \"commands\": [\n            {\n                \"name\": \"claim_list\",\n                \"description\": \"List my stream and channel claims.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"claim_type\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claim type: channel, stream, repost, collection\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claim id\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_id\",\n                        \"type\": \"str or list\",\n                        \"description\": \"streams in this channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"name\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claim name\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_spent\",\n                        \"type\": \"bool\",\n                        \"description\": \"shows previous claim updates and abandons\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to query\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict results to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"has_source\",\n                        \"type\": \"bool\",\n                        \"description\": \"list claims containing a source field\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"has_no_source\",\n                        \"type\": \"bool\",\n                        \"description\": \"list claims not containing a source field\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page\",\n                        \"type\": \"int\",\n                        \"description\": \"page to return during paginating\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page_size\",\n                        \"type\": \"int\",\n                        \"description\": \"number of items on page during pagination\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"resolve\",\n                        \"type\": \"bool\",\n                        \"description\": \"resolves each claim to provide additional metadata\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"order_by\",\n                        \"type\": \"str\",\n                        \"description\": \"field to order by: 'name', 'height', 'amount'\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"no_totals\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not calculate the total number of pages and items in result set (significant performance boost)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"include_received_tips\",\n                        \"type\": \"bool\",\n                        \"description\": \"calculate the amount of tips received for claim outputs\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"List all your claims\",\n                        \"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/\",\n                        \"lbrynet\": \"lbrynet claim list\",\n                        \"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()\",\n                        \"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}\"\n                    },\n                    {\n                        \"title\": \"Paginate your claims\",\n                        \"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/\",\n                        \"lbrynet\": \"lbrynet claim list --page=1 --page_size=20\",\n                        \"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()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"claim_search\",\n                \"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\\\"]\",\n                \"arguments\": [\n                    {\n                        \"name\": \"name\",\n                        \"type\": \"str\",\n                        \"description\": \"claim name (normalized)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"text\",\n                        \"type\": \"str\",\n                        \"description\": \"full text search\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str\",\n                        \"description\": \"full or partial claim id\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_ids\",\n                        \"type\": \"list\",\n                        \"description\": \"list of full claim ids\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"txid\",\n                        \"type\": \"str\",\n                        \"description\": \"transaction id\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"nout\",\n                        \"type\": \"str\",\n                        \"description\": \"position in the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel\",\n                        \"type\": \"str\",\n                        \"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\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_ids\",\n                        \"type\": \"list\",\n                        \"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\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"not_channel_ids\",\n                        \"type\": \"list\",\n                        \"description\": \"exclude claims signed by any of these channels (arguments must be claim ids of the channels)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"has_channel_signature\",\n                        \"type\": \"bool\",\n                        \"description\": \"claims with a channel signature (valid or invalid)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"valid_channel_signature\",\n                        \"type\": \"bool\",\n                        \"description\": \"claims with a valid channel signature or no signature, use in conjunction with --has_channel_signature to only get claims with valid signatures\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"invalid_channel_signature\",\n                        \"type\": \"bool\",\n                        \"description\": \"claims with invalid channel signature or no signature, use in conjunction with --has_channel_signature to only get claims with invalid signatures\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"limit_claims_per_channel\",\n                        \"type\": \"int\",\n                        \"description\": \"only return up to the specified number of claims per channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_controlling\",\n                        \"type\": \"bool\",\n                        \"description\": \"winning claims of their respective name\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"public_key_id\",\n                        \"type\": \"str\",\n                        \"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'}\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"height\",\n                        \"type\": \"int\",\n                        \"description\": \"last updated block height (supports equality constraints)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"timestamp\",\n                        \"type\": \"int\",\n                        \"description\": \"last updated timestamp (supports equality constraints)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"creation_height\",\n                        \"type\": \"int\",\n                        \"description\": \"created at block height (supports equality constraints)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"creation_timestamp\",\n                        \"type\": \"int\",\n                        \"description\": \"created at timestamp (supports equality constraints)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"activation_height\",\n                        \"type\": \"int\",\n                        \"description\": \"height at which claim starts competing for name (supports equality constraints)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"expiration_height\",\n                        \"type\": \"int\",\n                        \"description\": \"height at which claim will expire (supports equality constraints)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"release_time\",\n                        \"type\": \"int\",\n                        \"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)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"amount\",\n                        \"type\": \"int\",\n                        \"description\": \"limit by claim value (supports equality constraints)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"support_amount\",\n                        \"type\": \"int\",\n                        \"description\": \"limit by supports and tips received (supports equality constraints)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"effective_amount\",\n                        \"type\": \"int\",\n                        \"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)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"trending_score\",\n                        \"type\": \"int\",\n                        \"description\": \"limit by trending score (supports equality constraints)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"trending_group\",\n                        \"type\": \"int\",\n                        \"description\": \"DEPRECATED - instead please use trending_score\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"trending_mixed\",\n                        \"type\": \"int\",\n                        \"description\": \"DEPRECATED - instead please use trending_score\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"trending_local\",\n                        \"type\": \"int\",\n                        \"description\": \"DEPRECATED - instead please use trending_score\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"trending_global\",\n                        \"type\": \"int\",\n                        \"description\": \"DEPRECATED - instead please use trending_score\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"reposted_claim_id\",\n                        \"type\": \"str\",\n                        \"description\": \"all reposts of the specified original claim id\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"reposted\",\n                        \"type\": \"int\",\n                        \"description\": \"claims reposted this many times (supports equality constraints)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_type\",\n                        \"type\": \"str\",\n                        \"description\": \"filter by 'channel', 'stream', 'repost' or 'collection'\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"stream_types\",\n                        \"type\": \"list\",\n                        \"description\": \"filter by 'video', 'image', 'document', etc\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"media_types\",\n                        \"type\": \"list\",\n                        \"description\": \"filter by 'video/mp4', 'image/png', etc\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"fee_currency\",\n                        \"type\": \"string\",\n                        \"description\": \"specify fee currency: LBC, BTC, USD\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"fee_amount\",\n                        \"type\": \"decimal\",\n                        \"description\": \"content download fee (supports equality constraints)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"duration\",\n                        \"type\": \"int\",\n                        \"description\": \"duration of video or audio in seconds (supports equality constraints)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"any_tags\",\n                        \"type\": \"list\",\n                        \"description\": \"find claims containing any of the tags\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"all_tags\",\n                        \"type\": \"list\",\n                        \"description\": \"find claims containing every tag\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"not_tags\",\n                        \"type\": \"list\",\n                        \"description\": \"find claims not containing any of these tags\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"any_languages\",\n                        \"type\": \"list\",\n                        \"description\": \"find claims containing any of the languages\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"all_languages\",\n                        \"type\": \"list\",\n                        \"description\": \"find claims containing every language\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"not_languages\",\n                        \"type\": \"list\",\n                        \"description\": \"find claims not containing any of these languages\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"any_locations\",\n                        \"type\": \"list\",\n                        \"description\": \"find claims containing any of the locations\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"all_locations\",\n                        \"type\": \"list\",\n                        \"description\": \"find claims containing every location\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"not_locations\",\n                        \"type\": \"list\",\n                        \"description\": \"find claims not containing any of these locations\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page\",\n                        \"type\": \"int\",\n                        \"description\": \"page to return during paginating\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page_size\",\n                        \"type\": \"int\",\n                        \"description\": \"number of items on page during pagination\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"order_by\",\n                        \"type\": \"list\",\n                        \"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'\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"no_totals\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not calculate the total number of pages and items in result set (significant performance boost)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"wallet to check for claim purchase receipts\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"include_purchase_receipt\",\n                        \"type\": \"bool\",\n                        \"description\": \"lookup and include a receipt if this wallet has purchased the claim\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"include_is_my_output\",\n                        \"type\": \"bool\",\n                        \"description\": \"lookup and include a boolean indicating if claim being resolved is yours\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"remove_duplicates\",\n                        \"type\": \"bool\",\n                        \"description\": \"removes duplicated content from search by picking either the original claim or the oldest matching repost\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"has_source\",\n                        \"type\": \"bool\",\n                        \"description\": \"find claims containing a source field\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"sd_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"find claims where the source stream descriptor hash matches (partially or completely) the given hexadecimal string\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"has_no_source\",\n                        \"type\": \"bool\",\n                        \"description\": \"find claims not containing a source field\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"new_sdk_server\",\n                        \"type\": \"str\",\n                        \"description\": \"URL of the new SDK server (EXPERIMENTAL)\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"Search for all claims in channel\",\n                        \"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/\",\n                        \"lbrynet\": \"lbrynet claim search --channel=@channel\",\n                        \"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()\",\n                        \"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}\"\n                    },\n                    {\n                        \"title\": \"Search for claims matching a name\",\n                        \"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/\",\n                        \"lbrynet\": \"lbrynet claim search --name=\\\"astream\\\"\",\n                        \"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()\",\n                        \"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}\"\n                    }\n                ]\n            }\n        ]\n    },\n    \"collection\": {\n        \"doc\": \"Create, update, list, resolve, and abandon collections.\",\n        \"commands\": [\n            {\n                \"name\": \"collection_abandon\",\n                \"description\": \"Abandon one of my collection claims.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str\",\n                        \"description\": \"claim_id of the claim to abandon\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"txid\",\n                        \"type\": \"str\",\n                        \"description\": \"txid of the claim to abandon\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"nout\",\n                        \"type\": \"int\",\n                        \"description\": \"nout of the claim to abandon\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to use\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"preview\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not broadcast the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"blocking\",\n                        \"type\": \"bool\",\n                        \"description\": \"wait until abandon is in mempool\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"collection_create\",\n                \"description\": \"Create a new collection.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"name\",\n                        \"type\": \"str\",\n                        \"description\": \"name of the collection\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"bid\",\n                        \"type\": \"decimal\",\n                        \"description\": \"amount to back the claim\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"claims\",\n                        \"type\": \"list\",\n                        \"description\": \"claim ids to be included in the collection\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"allow_duplicate_name\",\n                        \"type\": \"bool\",\n                        \"description\": \"create new collection even if one already exists with given name. default: false.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"title\",\n                        \"type\": \"str\",\n                        \"description\": \"title of the collection\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"description\",\n                        \"type\": \"str\",\n                        \"description\": \"description of the collection\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"tags\",\n                        \"type\": \"list\",\n                        \"description\": \"content tags\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"clear_languages\",\n                        \"type\": \"bool\",\n                        \"description\": \"clear existing languages (prior to adding new ones)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"languages\",\n                        \"type\": \"list\",\n                        \"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`\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"locations\",\n                        \"type\": \"list\",\n                        \"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'}\\\"\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"thumbnail_url\",\n                        \"type\": \"str\",\n                        \"description\": \"thumbnail url\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_id\",\n                        \"type\": \"str\",\n                        \"description\": \"claim id of the publisher channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_name\",\n                        \"type\": \"str\",\n                        \"description\": \"name of the publisher channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"one or more account ids for accounts to look in for channel certificates, defaults to all accounts.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"account to use for holding the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"funding_account_ids\",\n                        \"type\": \"list\",\n                        \"description\": \"ids of accounts to fund this transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_address\",\n                        \"type\": \"str\",\n                        \"description\": \"address where the collection is sent to, if not specified it will be determined automatically from the account\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"preview\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not broadcast the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"blocking\",\n                        \"type\": \"bool\",\n                        \"description\": \"wait until transaction is in mempool\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"Create a collection of one stream\",\n                        \"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/\",\n                        \"lbrynet\": \"lbrynet collection create --name=tom --bid=1.0 --channel_id=595c2e2f0c1f59188628fab7503692b0145779a2 --claims=ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n                        \"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()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"collection_list\",\n                \"description\": \"List my collection claims.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"resolve\",\n                        \"type\": \"bool\",\n                        \"description\": \"resolve collection claim\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"resolve_claims\",\n                        \"type\": \"int\",\n                        \"description\": \"resolve every claim\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to use\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict results to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page\",\n                        \"type\": \"int\",\n                        \"description\": \"page to return during paginating\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page_size\",\n                        \"type\": \"int\",\n                        \"description\": \"number of items on page during pagination\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"List collections\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"collection_list\\\", \\\"params\\\": {\\\"resolve_claims\\\": 1, \\\"resolve\\\": true}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet collection list --resolve --resolve_claims=1\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"collection_list\\\", \\\"params\\\": {\\\"resolve_claims\\\": 1, \\\"resolve\\\": true}}).json()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"collection_resolve\",\n                \"description\": \"Resolve claims in the collection.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str\",\n                        \"description\": \"claim id of the collection\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"url\",\n                        \"type\": \"str\",\n                        \"description\": \"url of the collection\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict results to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page\",\n                        \"type\": \"int\",\n                        \"description\": \"page to return during paginating\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page_size\",\n                        \"type\": \"int\",\n                        \"description\": \"number of items on page during pagination\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"collection_update\",\n                \"description\": \"Update an existing collection claim.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str\",\n                        \"description\": \"claim_id of the collection to update\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"bid\",\n                        \"type\": \"decimal\",\n                        \"description\": \"amount to back the claim\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claims\",\n                        \"type\": \"list\",\n                        \"description\": \"claim ids\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"clear_claims\",\n                        \"type\": \"bool\",\n                        \"description\": \"clear existing claim references (prior to adding new ones)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"title\",\n                        \"type\": \"str\",\n                        \"description\": \"title of the collection\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"description\",\n                        \"type\": \"str\",\n                        \"description\": \"description of the collection\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"tags\",\n                        \"type\": \"list\",\n                        \"description\": \"add content tags\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"clear_tags\",\n                        \"type\": \"bool\",\n                        \"description\": \"clear existing tags (prior to adding new ones)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"languages\",\n                        \"type\": \"list\",\n                        \"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`\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"clear_languages\",\n                        \"type\": \"bool\",\n                        \"description\": \"clear existing languages (prior to adding new ones)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"locations\",\n                        \"type\": \"list\",\n                        \"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'}\\\"\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"clear_locations\",\n                        \"type\": \"bool\",\n                        \"description\": \"clear existing locations (prior to adding new ones)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"thumbnail_url\",\n                        \"type\": \"str\",\n                        \"description\": \"thumbnail url\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_id\",\n                        \"type\": \"str\",\n                        \"description\": \"claim id of the publisher channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_name\",\n                        \"type\": \"str\",\n                        \"description\": \"name of the publisher channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"one or more account ids for accounts to look in for channel certificates, defaults to all accounts.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"account in which to look for collection (default: all)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"funding_account_ids\",\n                        \"type\": \"list\",\n                        \"description\": \"ids of accounts to fund this transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_address\",\n                        \"type\": \"str\",\n                        \"description\": \"address where the collection is sent\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"preview\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not broadcast the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"blocking\",\n                        \"type\": \"bool\",\n                        \"description\": \"wait until transaction is in mempool\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"replace\",\n                        \"type\": \"bool\",\n                        \"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\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": []\n            }\n        ]\n    },\n    \"file\": {\n        \"doc\": \"File management.\",\n        \"commands\": [\n            {\n                \"name\": \"file_delete\",\n                \"description\": \"Delete a LBRY file\",\n                \"arguments\": [\n                    {\n                        \"name\": \"delete_from_download_dir\",\n                        \"type\": \"bool\",\n                        \"description\": \"delete file from download directory, instead of just deleting blobs\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"delete_all\",\n                        \"type\": \"bool\",\n                        \"description\": \"if there are multiple matching files, allow the deletion of multiple files. Otherwise do not delete anything.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"sd_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"delete by file sd hash\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"file_name\",\n                        \"type\": \"str\",\n                        \"description\": \"delete by file name in downloads folder\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"stream_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"delete by file stream hash\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"rowid\",\n                        \"type\": \"int\",\n                        \"description\": \"delete by file row id\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str\",\n                        \"description\": \"delete by file claim id\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"txid\",\n                        \"type\": \"str\",\n                        \"description\": \"delete by file claim txid\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"nout\",\n                        \"type\": \"int\",\n                        \"description\": \"delete by file claim nout\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_name\",\n                        \"type\": \"str\",\n                        \"description\": \"delete by file claim name\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_claim_id\",\n                        \"type\": \"str\",\n                        \"description\": \"delete by file channel claim id\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_name\",\n                        \"type\": \"str\",\n                        \"description\": \"delete by file channel claim name\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(bool) true if deletion was successful\",\n                \"examples\": [\n                    {\n                        \"title\": \"Delete a file\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"file_delete\\\", \\\"params\\\": {\\\"delete_from_download_dir\\\": false, \\\"delete_all\\\": false, \\\"claim_id\\\": \\\"ad25e05aa7dc5e9994869040c6103f9a8728db46\\\"}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet file delete --claim_id=\\\"ad25e05aa7dc5e9994869040c6103f9a8728db46\\\"\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"file_delete\\\", \\\"params\\\": {\\\"delete_from_download_dir\\\": false, \\\"delete_all\\\": false, \\\"claim_id\\\": \\\"ad25e05aa7dc5e9994869040c6103f9a8728db46\\\"}}).json()\",\n                        \"output\": \"{\\n  \\\"jsonrpc\\\": \\\"2.0\\\",\\n  \\\"result\\\": true\\n}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"file_list\",\n                \"description\": \"List files limited by optional filters\",\n                \"arguments\": [\n                    {\n                        \"name\": \"sd_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"get file with matching sd hash\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"file_name\",\n                        \"type\": \"str\",\n                        \"description\": \"get file with matching file name in the downloads folder\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"stream_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"get file with matching stream hash\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"rowid\",\n                        \"type\": \"int\",\n                        \"description\": \"get file with matching row id\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"added_on\",\n                        \"type\": \"int\",\n                        \"description\": \"get file with matching time of insertion\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str\",\n                        \"description\": \"get file with matching claim id(s)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"outpoint\",\n                        \"type\": \"str\",\n                        \"description\": \"get file with matching claim outpoint(s)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"txid\",\n                        \"type\": \"str\",\n                        \"description\": \"get file with matching claim txid\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"nout\",\n                        \"type\": \"int\",\n                        \"description\": \"get file with matching claim nout\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_claim_id\",\n                        \"type\": \"str\",\n                        \"description\": \"get file with matching channel claim id(s)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_name\",\n                        \"type\": \"str\",\n                        \"description\": \"get file with matching channel name\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_name\",\n                        \"type\": \"str\",\n                        \"description\": \"get file with matching claim name\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"blobs_in_stream\",\n                        \"type\": \"int\",\n                        \"description\": \"get file with matching blobs in stream\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"download_path\",\n                        \"type\": \"str\",\n                        \"description\": \"get file with matching download path\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"uploading_to_reflector\",\n                        \"type\": \"bool\",\n                        \"description\": \"get files currently uploading to reflector\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_fully_reflected\",\n                        \"type\": \"bool\",\n                        \"description\": \"get files that have been uploaded to reflector\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"status\",\n                        \"type\": \"str\",\n                        \"description\": \"match by status, ( running | finished | stopped )\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"completed\",\n                        \"type\": \"bool\",\n                        \"description\": \"match only completed\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"blobs_remaining\",\n                        \"type\": \"int\",\n                        \"description\": \"amount of remaining blobs to download\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"sort\",\n                        \"type\": \"str\",\n                        \"description\": \"field to sort by (one of the above filter fields)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"comparison\",\n                        \"type\": \"str\",\n                        \"description\": \"logical comparison, (eq | ne | g | ge | l | le | in)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page\",\n                        \"type\": \"int\",\n                        \"description\": \"page to return during paginating\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page_size\",\n                        \"type\": \"int\",\n                        \"description\": \"number of items on page during pagination\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"add purchase receipts from this wallet\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"List local files\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"file_list\\\", \\\"params\\\": {\\\"reverse\\\": false}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet file list\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"file_list\\\", \\\"params\\\": {\\\"reverse\\\": false}}).json()\",\n                        \"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}\"\n                    },\n                    {\n                        \"title\": \"List files matching a parameter\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"file_list\\\", \\\"params\\\": {\\\"claim_id\\\": \\\"ad25e05aa7dc5e9994869040c6103f9a8728db46\\\", \\\"reverse\\\": false}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet file list --claim_id=\\\"ad25e05aa7dc5e9994869040c6103f9a8728db46\\\"\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"file_list\\\", \\\"params\\\": {\\\"claim_id\\\": \\\"ad25e05aa7dc5e9994869040c6103f9a8728db46\\\", \\\"reverse\\\": false}}).json()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"file_reflect\",\n                \"description\": \"Reflect all the blobs in a file matching the filter criteria\",\n                \"arguments\": [\n                    {\n                        \"name\": \"sd_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"get file with matching sd hash\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"file_name\",\n                        \"type\": \"str\",\n                        \"description\": \"get file with matching file name in the downloads folder\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"stream_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"get file with matching stream hash\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"rowid\",\n                        \"type\": \"int\",\n                        \"description\": \"get file with matching row id\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"reflector\",\n                        \"type\": \"str\",\n                        \"description\": \"reflector server, ip address or url by default choose a server from the config\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(list) list of blobs reflected\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"file_save\",\n                \"description\": \"Start saving a file to disk.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"file_name\",\n                        \"type\": \"str\",\n                        \"description\": \"file name to save to\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"download_directory\",\n                        \"type\": \"str\",\n                        \"description\": \"directory to save into\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"sd_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"save file with matching sd hash\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"stream_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"save file with matching stream hash\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"rowid\",\n                        \"type\": \"int\",\n                        \"description\": \"save file with matching row id\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str\",\n                        \"description\": \"save file with matching claim id\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"txid\",\n                        \"type\": \"str\",\n                        \"description\": \"save file with matching claim txid\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"nout\",\n                        \"type\": \"int\",\n                        \"description\": \"save file with matching claim nout\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_name\",\n                        \"type\": \"str\",\n                        \"description\": \"save file with matching claim name\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_claim_id\",\n                        \"type\": \"str\",\n                        \"description\": \"save file with matching channel claim id\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_name\",\n                        \"type\": \"str\",\n                        \"description\": \"save file with matching channel claim name\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"Save a file to the downloads directory\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"file_save\\\", \\\"params\\\": {\\\"sd_hash\\\": \\\"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\\\"}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet file save --sd_hash=\\\"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\\\"\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"file_save\\\", \\\"params\\\": {\\\"sd_hash\\\": \\\"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\\\"}}).json()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"file_set_status\",\n                \"description\": \"Start or stop downloading a file\",\n                \"arguments\": [\n                    {\n                        \"name\": \"status\",\n                        \"type\": \"str\",\n                        \"description\": \"one of \\\"start\\\" or \\\"stop\\\"\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"sd_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"set status of file with matching sd hash\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"file_name\",\n                        \"type\": \"str\",\n                        \"description\": \"set status of file with matching file name in the downloads folder\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"stream_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"set status of file with matching stream hash\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"rowid\",\n                        \"type\": \"int\",\n                        \"description\": \"set status of file with matching row id\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(str) Confirmation message\",\n                \"examples\": []\n            }\n        ]\n    },\n    \"peer\": {\n        \"doc\": \"DHT / Blob Exchange peer commands.\",\n        \"commands\": [\n            {\n                \"name\": \"peer_list\",\n                \"description\": \"Get peers for blob hash\",\n                \"arguments\": [\n                    {\n                        \"name\": \"blob_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"find available peers for this blob hash\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"page\",\n                        \"type\": \"int\",\n                        \"description\": \"page to return during paginating\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page_size\",\n                        \"type\": \"int\",\n                        \"description\": \"number of items on page during pagination\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(list) List of contact dictionaries {'address': <peer ip>, 'udp_port': <dht port>, 'tcp_port': <peer port>,\\n     'node_id': <peer node id>}\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"peer_ping\",\n                \"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.\",\n                \"arguments\": [],\n                \"returns\": \"(str) pong, or {'error': <error message>} if an error is encountered\",\n                \"examples\": []\n            }\n        ]\n    },\n    \"preference\": {\n        \"doc\": \"Preferences management.\",\n        \"commands\": [\n            {\n                \"name\": \"preference_get\",\n                \"description\": \"Get preference value for key or all values if not key is passed in.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"key\",\n                        \"type\": \"str\",\n                        \"description\": \"key associated with value\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(dict) Dictionary of preference(s)\",\n                \"examples\": [\n                    {\n                        \"title\": \"Get preferences\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"preference_get\\\", \\\"params\\\": {}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet preference get\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"preference_get\\\", \\\"params\\\": {}}).json()\",\n                        \"output\": \"{\\n  \\\"jsonrpc\\\": \\\"2.0\\\",\\n  \\\"result\\\": {\\n    \\\"theme\\\": \\\"dark\\\"\\n  }\\n}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"preference_set\",\n                \"description\": \"Set preferences\",\n                \"arguments\": [\n                    {\n                        \"name\": \"key\",\n                        \"type\": \"str\",\n                        \"description\": \"key associated with value\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"value\",\n                        \"type\": \"str\",\n                        \"description\": \"key associated with value\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(dict) Dictionary with key/value of new preference\",\n                \"examples\": [\n                    {\n                        \"title\": \"Set preference\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"preference_set\\\", \\\"params\\\": {\\\"key\\\": \\\"theme\\\", \\\"value\\\": \\\"dark\\\"}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet preference set \\\"theme\\\" \\\"dark\\\"\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"preference_set\\\", \\\"params\\\": {\\\"key\\\": \\\"theme\\\", \\\"value\\\": \\\"dark\\\"}}).json()\",\n                        \"output\": \"{\\n  \\\"jsonrpc\\\": \\\"2.0\\\",\\n  \\\"result\\\": {\\n    \\\"theme\\\": \\\"dark\\\"\\n  }\\n}\"\n                    }\n                ]\n            }\n        ]\n    },\n    \"purchase\": {\n        \"doc\": \"List and make purchases of claims.\",\n        \"commands\": [\n            {\n                \"name\": \"purchase_create\",\n                \"description\": \"Purchase a claim.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str\",\n                        \"description\": \"claim id of claim to purchase\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"url\",\n                        \"type\": \"str\",\n                        \"description\": \"lookup claim to purchase by url\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"funding_account_ids\",\n                        \"type\": \"list\",\n                        \"description\": \"ids of accounts to fund this transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"allow_duplicate_purchase\",\n                        \"type\": \"bool\",\n                        \"description\": \"allow purchasing claim_id you already own\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"override_max_key_fee\",\n                        \"type\": \"bool\",\n                        \"description\": \"ignore max key fee for this purchase\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"preview\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not broadcast the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"blocking\",\n                        \"type\": \"bool\",\n                        \"description\": \"wait until transaction is in mempool\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"purchase_list\",\n                \"description\": \"List my claim purchases.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str\",\n                        \"description\": \"purchases for specific claim\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"resolve\",\n                        \"type\": \"str\",\n                        \"description\": \"include resolved claim information\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to query\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict results to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page\",\n                        \"type\": \"int\",\n                        \"description\": \"page to return during paginating\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page_size\",\n                        \"type\": \"int\",\n                        \"description\": \"number of items on page during pagination\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": []\n            }\n        ]\n    },\n    \"settings\": {\n        \"doc\": \"Settings management.\",\n        \"commands\": [\n            {\n                \"name\": \"settings_clear\",\n                \"description\": \"Clear daemon settings\",\n                \"arguments\": [],\n                \"returns\": \"(dict) Updated dictionary of daemon settings\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"settings_get\",\n                \"description\": \"Get daemon settings\",\n                \"arguments\": [],\n                \"returns\": \"(dict) Dictionary of daemon settings\\n    See ADJUSTABLE_SETTINGS in lbry/conf.py for full list of settings\",\n                \"examples\": [\n                    {\n                        \"title\": \"Get settings\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"settings_get\\\", \\\"params\\\": {}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet settings get\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"settings_get\\\", \\\"params\\\": {}}).json()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"settings_set\",\n                \"description\": \"Set daemon settings\",\n                \"arguments\": [],\n                \"returns\": \"(dict) Updated dictionary of daemon settings\",\n                \"examples\": [\n                    {\n                        \"title\": \"Set settings\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"settings_set\\\", \\\"params\\\": {\\\"key\\\": \\\"tcp_port\\\", \\\"value\\\": 99}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet settings set \\\"tcp_port\\\" 99\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"settings_set\\\", \\\"params\\\": {\\\"key\\\": \\\"tcp_port\\\", \\\"value\\\": 99}}).json()\",\n                        \"output\": \"{\\n  \\\"jsonrpc\\\": \\\"2.0\\\",\\n  \\\"result\\\": {\\n    \\\"tcp_port\\\": 99\\n  }\\n}\"\n                    }\n                ]\n            }\n        ]\n    },\n    \"stream\": {\n        \"doc\": \"Create, update, abandon, list and inspect your stream claims.\",\n        \"commands\": [\n            {\n                \"name\": \"stream_abandon\",\n                \"description\": \"Abandon one of my stream claims.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str\",\n                        \"description\": \"claim_id of the claim to abandon\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"txid\",\n                        \"type\": \"str\",\n                        \"description\": \"txid of the claim to abandon\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"nout\",\n                        \"type\": \"int\",\n                        \"description\": \"nout of the claim to abandon\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to use\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"preview\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not broadcast the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"blocking\",\n                        \"type\": \"bool\",\n                        \"description\": \"wait until abandon is in mempool\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"Abandon a stream claim\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"stream_abandon\\\", \\\"params\\\": {\\\"claim_id\\\": \\\"ad25e05aa7dc5e9994869040c6103f9a8728db46\\\", \\\"preview\\\": false, \\\"blocking\\\": false}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet stream abandon ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"stream_abandon\\\", \\\"params\\\": {\\\"claim_id\\\": \\\"ad25e05aa7dc5e9994869040c6103f9a8728db46\\\", \\\"preview\\\": false, \\\"blocking\\\": false}}).json()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"stream_cost_estimate\",\n                \"description\": \"Get estimated cost for a lbry stream\",\n                \"arguments\": [\n                    {\n                        \"name\": \"uri\",\n                        \"type\": \"str\",\n                        \"description\": \"uri to use\",\n                        \"is_required\": true\n                    }\n                ],\n                \"returns\": \"(float) Estimated cost in lbry credits, returns None if uri is not\\n        resolvable\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"stream_create\",\n                \"description\": \"Make a new stream claim and announce the associated file to lbrynet.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"name\",\n                        \"type\": \"str\",\n                        \"description\": \"name of the content (can only consist of a-z A-Z 0-9 and -(dash))\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"bid\",\n                        \"type\": \"decimal\",\n                        \"description\": \"amount to back the claim\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"file_path\",\n                        \"type\": \"str\",\n                        \"description\": \"path to file to be associated with name.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"file_name\",\n                        \"type\": \"str\",\n                        \"description\": \"name of file to be associated with stream.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"file_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"hash of file to be associated with stream.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"validate_file\",\n                        \"type\": \"bool\",\n                        \"description\": \"validate that the video container and encodings match common web browser support or that optimization succeeds if specified. FFmpeg is required\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"optimize_file\",\n                        \"type\": \"bool\",\n                        \"description\": \"transcode the video & audio if necessary to ensure common web browser support. FFmpeg is required\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"allow_duplicate_name\",\n                        \"type\": \"bool\",\n                        \"description\": \"create new claim even if one already exists with given name. default: false.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"fee_currency\",\n                        \"type\": \"string\",\n                        \"description\": \"specify fee currency\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"fee_amount\",\n                        \"type\": \"decimal\",\n                        \"description\": \"content download fee\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"fee_address\",\n                        \"type\": \"str\",\n                        \"description\": \"address where to send fee payments, will use value from --claim_address if not provided\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"title\",\n                        \"type\": \"str\",\n                        \"description\": \"title of the publication\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"description\",\n                        \"type\": \"str\",\n                        \"description\": \"description of the publication\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"author\",\n                        \"type\": \"str\",\n                        \"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\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"tags\",\n                        \"type\": \"list\",\n                        \"description\": \"add content tags\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"languages\",\n                        \"type\": \"list\",\n                        \"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`\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"locations\",\n                        \"type\": \"list\",\n                        \"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'}\\\"\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"license\",\n                        \"type\": \"str\",\n                        \"description\": \"publication license\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"license_url\",\n                        \"type\": \"str\",\n                        \"description\": \"publication license url\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"thumbnail_url\",\n                        \"type\": \"str\",\n                        \"description\": \"thumbnail url\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"release_time\",\n                        \"type\": \"int\",\n                        \"description\": \"original public release of content, seconds since UNIX epoch\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"width\",\n                        \"type\": \"int\",\n                        \"description\": \"image/video width, automatically calculated from media file\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"height\",\n                        \"type\": \"int\",\n                        \"description\": \"image/video height, automatically calculated from media file\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"duration\",\n                        \"type\": \"int\",\n                        \"description\": \"audio/video duration in seconds, automatically calculated\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"sd_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"sd_hash of stream\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_id\",\n                        \"type\": \"str\",\n                        \"description\": \"claim id of the publisher channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_name\",\n                        \"type\": \"str\",\n                        \"description\": \"name of the publisher channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"one or more account ids for accounts to look in for channel certificates, defaults to all accounts.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"account to use for holding the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"funding_account_ids\",\n                        \"type\": \"list\",\n                        \"description\": \"ids of accounts to fund this transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_address\",\n                        \"type\": \"str\",\n                        \"description\": \"address where the claim is sent to, if not specified it will be determined automatically from the account\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"preview\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not broadcast the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"blocking\",\n                        \"type\": \"bool\",\n                        \"description\": \"wait until transaction is in mempool\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"Create a stream claim without metadata\",\n                        \"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/\",\n                        \"lbrynet\": \"lbrynet stream create astream 1.0 /var/folders/46/44w2zhrx16b8gsvff9dxtr640000gq/T/tmpr832hp1x\",\n                        \"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()\",\n                        \"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}\"\n                    },\n                    {\n                        \"title\": \"Create an image stream claim with all metadata and fee\",\n                        \"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/\",\n                        \"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\\\"\",\n                        \"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()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"stream_list\",\n                \"description\": \"List my stream claims.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"name\",\n                        \"type\": \"str or list\",\n                        \"description\": \"stream name\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str or list\",\n                        \"description\": \"stream id\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_spent\",\n                        \"type\": \"bool\",\n                        \"description\": \"shows previous stream updates and abandons\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to query\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict results to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page\",\n                        \"type\": \"int\",\n                        \"description\": \"page to return during paginating\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page_size\",\n                        \"type\": \"int\",\n                        \"description\": \"number of items on page during pagination\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"resolve\",\n                        \"type\": \"bool\",\n                        \"description\": \"resolves each stream to provide additional metadata\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"no_totals\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not calculate the total number of pages and items in result set (significant performance boost)\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"List all your stream claims\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"stream_list\\\", \\\"params\\\": {\\\"name\\\": [], \\\"claim_id\\\": [], \\\"is_spent\\\": false, \\\"resolve\\\": false, \\\"no_totals\\\": false}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet stream list\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"stream_list\\\", \\\"params\\\": {\\\"name\\\": [], \\\"claim_id\\\": [], \\\"is_spent\\\": false, \\\"resolve\\\": false, \\\"no_totals\\\": false}}).json()\",\n                        \"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}\"\n                    },\n                    {\n                        \"title\": \"Paginate your stream claims\",\n                        \"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/\",\n                        \"lbrynet\": \"lbrynet stream list --page=1 --page_size=20\",\n                        \"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()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"stream_repost\",\n                \"description\": \"Creates a claim that references an existing stream by its claim id.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"name\",\n                        \"type\": \"str\",\n                        \"description\": \"name of the content (can only consist of a-z A-Z 0-9 and -(dash))\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"bid\",\n                        \"type\": \"decimal\",\n                        \"description\": \"amount to back the claim\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the claim being reposted\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"allow_duplicate_name\",\n                        \"type\": \"bool\",\n                        \"description\": \"create new claim even if one already exists with given name. default: false.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"title\",\n                        \"type\": \"str\",\n                        \"description\": \"title of the repost\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"description\",\n                        \"type\": \"str\",\n                        \"description\": \"description of the repost\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"tags\",\n                        \"type\": \"list\",\n                        \"description\": \"add repost tags\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_id\",\n                        \"type\": \"str\",\n                        \"description\": \"claim id of the publisher channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_name\",\n                        \"type\": \"str\",\n                        \"description\": \"name of the publisher channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"one or more account ids for accounts to look in for channel certificates, defaults to all accounts.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"account to use for holding the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"funding_account_ids\",\n                        \"type\": \"list\",\n                        \"description\": \"ids of accounts to fund this transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_address\",\n                        \"type\": \"str\",\n                        \"description\": \"address where the claim is sent to, if not specified it will be determined automatically from the account\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"preview\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not broadcast the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"blocking\",\n                        \"type\": \"bool\",\n                        \"description\": \"wait until transaction is in mempool\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"stream_update\",\n                \"description\": \"Update an existing stream claim and if a new file is provided announce it to lbrynet.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the stream claim to update\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"bid\",\n                        \"type\": \"decimal\",\n                        \"description\": \"amount to back the claim\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"file_path\",\n                        \"type\": \"str\",\n                        \"description\": \"path to file to be associated with name.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"validate_file\",\n                        \"type\": \"bool\",\n                        \"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.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"optimize_file\",\n                        \"type\": \"bool\",\n                        \"description\": \"transcode the video & audio if necessary to ensure common web browser support. FFmpeg is required and file_path must be specified.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"file_name\",\n                        \"type\": \"str\",\n                        \"description\": \"override file name, defaults to name from file_path.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"file_size\",\n                        \"type\": \"str\",\n                        \"description\": \"override file size, otherwise automatically computed.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"file_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"override file hash, otherwise automatically computed.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"fee_currency\",\n                        \"type\": \"string\",\n                        \"description\": \"specify fee currency\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"fee_amount\",\n                        \"type\": \"decimal\",\n                        \"description\": \"content download fee\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"fee_address\",\n                        \"type\": \"str\",\n                        \"description\": \"address where to send fee payments, will use value from --claim_address if not provided\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"clear_fee\",\n                        \"type\": \"bool\",\n                        \"description\": \"clear previously set fee\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"title\",\n                        \"type\": \"str\",\n                        \"description\": \"title of the publication\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"description\",\n                        \"type\": \"str\",\n                        \"description\": \"description of the publication\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"author\",\n                        \"type\": \"str\",\n                        \"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\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"tags\",\n                        \"type\": \"list\",\n                        \"description\": \"add content tags\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"clear_tags\",\n                        \"type\": \"bool\",\n                        \"description\": \"clear existing tags (prior to adding new ones)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"languages\",\n                        \"type\": \"list\",\n                        \"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`\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"clear_languages\",\n                        \"type\": \"bool\",\n                        \"description\": \"clear existing languages (prior to adding new ones)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"locations\",\n                        \"type\": \"list\",\n                        \"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'}\\\"\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"clear_locations\",\n                        \"type\": \"bool\",\n                        \"description\": \"clear existing locations (prior to adding new ones)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"license\",\n                        \"type\": \"str\",\n                        \"description\": \"publication license\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"license_url\",\n                        \"type\": \"str\",\n                        \"description\": \"publication license url\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"thumbnail_url\",\n                        \"type\": \"str\",\n                        \"description\": \"thumbnail url\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"release_time\",\n                        \"type\": \"int\",\n                        \"description\": \"original public release of content, seconds since UNIX epoch\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"width\",\n                        \"type\": \"int\",\n                        \"description\": \"image/video width, automatically calculated from media file\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"height\",\n                        \"type\": \"int\",\n                        \"description\": \"image/video height, automatically calculated from media file\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"duration\",\n                        \"type\": \"int\",\n                        \"description\": \"audio/video duration in seconds, automatically calculated\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"sd_hash\",\n                        \"type\": \"str\",\n                        \"description\": \"sd_hash of stream\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_id\",\n                        \"type\": \"str\",\n                        \"description\": \"claim id of the publisher channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_name\",\n                        \"type\": \"str\",\n                        \"description\": \"name of the publisher channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"clear_channel\",\n                        \"type\": \"bool\",\n                        \"description\": \"remove channel signature\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"one or more account ids for accounts to look in for channel certificates, defaults to all accounts.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"account in which to look for stream (default: all)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"funding_account_ids\",\n                        \"type\": \"list\",\n                        \"description\": \"ids of accounts to fund this transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_address\",\n                        \"type\": \"str\",\n                        \"description\": \"address where the claim is sent to, if not specified it will be determined automatically from the account\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"preview\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not broadcast the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"blocking\",\n                        \"type\": \"bool\",\n                        \"description\": \"wait until transaction is in mempool\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"replace\",\n                        \"type\": \"bool\",\n                        \"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\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"Update a stream claim to add channel\",\n                        \"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/\",\n                        \"lbrynet\": \"lbrynet stream update ad25e05aa7dc5e9994869040c6103f9a8728db46 --channel_id=\\\"595c2e2f0c1f59188628fab7503692b0145779a2\\\"\",\n                        \"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()\",\n                        \"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}\"\n                    }\n                ]\n            }\n        ]\n    },\n    \"support\": {\n        \"doc\": \"Create, list and abandon all types of supports.\",\n        \"commands\": [\n            {\n                \"name\": \"support_abandon\",\n                \"description\": \"Abandon supports, including tips, of a specific claim, optionally\\nkeeping some amount as supports.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str\",\n                        \"description\": \"claim_id of the support to abandon\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"txid\",\n                        \"type\": \"str\",\n                        \"description\": \"txid of the claim to abandon\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"nout\",\n                        \"type\": \"int\",\n                        \"description\": \"nout of the claim to abandon\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"keep\",\n                        \"type\": \"decimal\",\n                        \"description\": \"amount of lbc to keep as support\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to use\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"preview\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not broadcast the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"blocking\",\n                        \"type\": \"bool\",\n                        \"description\": \"wait until abandon is in mempool\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"support_create\",\n                \"description\": \"Create a support or a tip for name claim.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str\",\n                        \"description\": \"claim_id of the claim to support\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"amount\",\n                        \"type\": \"decimal\",\n                        \"description\": \"amount of support\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"tip\",\n                        \"type\": \"bool\",\n                        \"description\": \"send support to claim owner, default: false.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_id\",\n                        \"type\": \"str\",\n                        \"description\": \"claim id of the supporters identity channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_name\",\n                        \"type\": \"str\",\n                        \"description\": \"name of the supporters identity channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"one or more account ids for accounts to look in for channel certificates, defaults to all accounts.\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"account to use for holding the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"funding_account_ids\",\n                        \"type\": \"list\",\n                        \"description\": \"ids of accounts to fund this transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"comment\",\n                        \"type\": \"str\",\n                        \"description\": \"add a comment to the support\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"preview\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not broadcast the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"blocking\",\n                        \"type\": \"bool\",\n                        \"description\": \"wait until transaction is in mempool\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"support_list\",\n                \"description\": \"List staked supports and sent/received tips.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"name\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claim name\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claim id\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"received\",\n                        \"type\": \"bool\",\n                        \"description\": \"only show received (tips)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"sent\",\n                        \"type\": \"bool\",\n                        \"description\": \"only show sent (tips)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"staked\",\n                        \"type\": \"bool\",\n                        \"description\": \"only show my staked supports\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_spent\",\n                        \"type\": \"bool\",\n                        \"description\": \"show abandoned supports\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to query\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict results to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page\",\n                        \"type\": \"int\",\n                        \"description\": \"page to return during paginating\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page_size\",\n                        \"type\": \"int\",\n                        \"description\": \"number of items on page during pagination\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"no_totals\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not calculate the total number of pages and items in result set (significant performance boost)\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"support_sum\",\n                \"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 !!!!!\",\n                \"arguments\": [\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str\",\n                        \"description\": \"claim id\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"new_sdk_server\",\n                        \"type\": \"str\",\n                        \"description\": \"URL of the new SDK server (EXPERIMENTAL)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"include_channel_content\",\n                        \"type\": \"bool\",\n                        \"description\": \"if claim_id is for a channel, include supports for claims in that channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page\",\n                        \"type\": \"int\",\n                        \"description\": \"page to return during paginating\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page_size\",\n                        \"type\": \"int\",\n                        \"description\": \"number of items on page during pagination\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": []\n            }\n        ]\n    },\n    \"sync\": {\n        \"doc\": \"Wallet synchronization.\",\n        \"commands\": [\n            {\n                \"name\": \"sync_apply\",\n                \"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).\",\n                \"arguments\": [\n                    {\n                        \"name\": \"password\",\n                        \"type\": \"str\",\n                        \"description\": \"password to decrypt incoming and encrypt outgoing data\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"data\",\n                        \"type\": \"str\",\n                        \"description\": \"incoming sync data, if any\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"wallet being sync'ed\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"blocking\",\n                        \"type\": \"bool\",\n                        \"description\": \"wait until any new accounts have sync'ed\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(map) sync hash and data\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"sync_hash\",\n                \"description\": \"Deterministic hash of the wallet.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"wallet for which to generate hash\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(str) sha256 hash of wallet\",\n                \"examples\": []\n            }\n        ]\n    },\n    \"tracemalloc\": {\n        \"doc\": \"Controls and queries tracemalloc memory tracing tools for troubleshooting.\",\n        \"commands\": [\n            {\n                \"name\": \"tracemalloc_disable\",\n                \"description\": \"Disable tracemalloc memory tracing\",\n                \"arguments\": [],\n                \"returns\": \"(bool) is it tracing?\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"tracemalloc_enable\",\n                \"description\": \"Enable tracemalloc memory tracing\",\n                \"arguments\": [],\n                \"returns\": \"(bool) is it tracing?\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"tracemalloc_top\",\n                \"description\": \"Show most common objects, the place that created them and their size.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"items\",\n                        \"type\": \"int\",\n                        \"description\": \"maximum items to return, from the most common\",\n                        \"is_required\": true\n                    }\n                ],\n                \"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    }\",\n                \"examples\": []\n            }\n        ]\n    },\n    \"transaction\": {\n        \"doc\": \"Transaction management.\",\n        \"commands\": [\n            {\n                \"name\": \"transaction_list\",\n                \"description\": \"List transactions belonging to wallet\",\n                \"arguments\": [\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to query\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict results to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page\",\n                        \"type\": \"int\",\n                        \"description\": \"page to return during paginating\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page_size\",\n                        \"type\": \"int\",\n                        \"description\": \"number of items on page during pagination\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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    }\",\n                \"examples\": [\n                    {\n                        \"title\": \"List your transactions\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"transaction_list\\\", \\\"params\\\": {}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet transaction list\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"transaction_list\\\", \\\"params\\\": {}}).json()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"transaction_show\",\n                \"description\": \"Get a decoded transaction from a txid\",\n                \"arguments\": [\n                    {\n                        \"name\": \"txid\",\n                        \"type\": \"str\",\n                        \"description\": \"txid of the transaction\",\n                        \"is_required\": true\n                    }\n                ],\n                \"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            }\",\n                \"examples\": []\n            }\n        ]\n    },\n    \"txo\": {\n        \"doc\": \"List and sum transaction outputs.\",\n        \"commands\": [\n            {\n                \"name\": \"txo_list\",\n                \"description\": \"List my transaction outputs.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"type\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claim type: stream, channel, support, purchase, collection, repost, other\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"txid\",\n                        \"type\": \"str or list\",\n                        \"description\": \"transaction id of outputs\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claim id\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_id\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claims in this channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"not_channel_id\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claims not in this channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"name\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claim name\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_spent\",\n                        \"type\": \"bool\",\n                        \"description\": \"only show spent txos\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_not_spent\",\n                        \"type\": \"bool\",\n                        \"description\": \"only show not spent txos\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_my_input_or_output\",\n                        \"type\": \"bool\",\n                        \"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)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_my_output\",\n                        \"type\": \"bool\",\n                        \"description\": \"show outputs controlled by you\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_not_my_output\",\n                        \"type\": \"bool\",\n                        \"description\": \"show outputs not controlled by you\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_my_input\",\n                        \"type\": \"bool\",\n                        \"description\": \"show outputs created by you\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_not_my_input\",\n                        \"type\": \"bool\",\n                        \"description\": \"show outputs not created by you\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"exclude_internal_transfers\",\n                        \"type\": \"bool\",\n                        \"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\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"include_received_tips\",\n                        \"type\": \"bool\",\n                        \"description\": \"calculate the amount of tips received for claim outputs\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to query\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict results to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page\",\n                        \"type\": \"int\",\n                        \"description\": \"page to return during paginating\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page_size\",\n                        \"type\": \"int\",\n                        \"description\": \"number of items on page during pagination\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"resolve\",\n                        \"type\": \"bool\",\n                        \"description\": \"resolves each claim to provide additional metadata\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"order_by\",\n                        \"type\": \"str\",\n                        \"description\": \"field to order by: 'name', 'height', 'amount' and 'none'\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"no_totals\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not calculate the total number of pages and items in result set (significant performance boost)\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"txo_plot\",\n                \"description\": \"Plot transaction output sum over days.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"type\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claim type: stream, channel, support, purchase, collection, repost, other\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"txid\",\n                        \"type\": \"str or list\",\n                        \"description\": \"transaction id of outputs\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claim id\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"name\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claim name\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_id\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claims in this channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"not_channel_id\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claims not in this channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_spent\",\n                        \"type\": \"bool\",\n                        \"description\": \"only show spent txos\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_not_spent\",\n                        \"type\": \"bool\",\n                        \"description\": \"only show not spent txos\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_my_input_or_output\",\n                        \"type\": \"bool\",\n                        \"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)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_my_output\",\n                        \"type\": \"bool\",\n                        \"description\": \"show outputs controlled by you\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_not_my_output\",\n                        \"type\": \"bool\",\n                        \"description\": \"show outputs not controlled by you\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_my_input\",\n                        \"type\": \"bool\",\n                        \"description\": \"show outputs created by you\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_not_my_input\",\n                        \"type\": \"bool\",\n                        \"description\": \"show outputs not created by you\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"exclude_internal_transfers\",\n                        \"type\": \"bool\",\n                        \"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\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to query\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict results to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"days_back\",\n                        \"type\": \"int\",\n                        \"description\": \"number of days back from today (not compatible with --start_day, --days_after, --end_day)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"start_day\",\n                        \"type\": \"date\",\n                        \"description\": \"start on specific date (YYYY-MM-DD) (instead of --days_back)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"days_after\",\n                        \"type\": \"int\",\n                        \"description\": \"end number of days after --start_day (instead of --end_day)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"end_day\",\n                        \"type\": \"date\",\n                        \"description\": \"end on specific date (YYYY-MM-DD) (instead of --days_after)\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"List[Dict]\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"txo_spend\",\n                \"description\": \"Spend transaction outputs, batching into multiple transactions as necessary.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"type\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claim type: stream, channel, support, purchase, collection, repost, other\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"txid\",\n                        \"type\": \"str or list\",\n                        \"description\": \"transaction id of outputs\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claim id\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_id\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claims in this channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"not_channel_id\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claims not in this channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"name\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claim name\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_my_input\",\n                        \"type\": \"bool\",\n                        \"description\": \"show outputs created by you\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_not_my_input\",\n                        \"type\": \"bool\",\n                        \"description\": \"show outputs not created by you\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"exclude_internal_transfers\",\n                        \"type\": \"bool\",\n                        \"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\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to query\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict results to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"preview\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not broadcast the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"blocking\",\n                        \"type\": \"bool\",\n                        \"description\": \"wait until abandon is in mempool\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"batch_size\",\n                        \"type\": \"int\",\n                        \"description\": \"number of txos to spend per transactions\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"include_full_tx\",\n                        \"type\": \"bool\",\n                        \"description\": \"include entire tx in output and not just the txid\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            ]\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"txo_sum\",\n                \"description\": \"Sum of transaction outputs.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"type\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claim type: stream, channel, support, purchase, collection, repost, other\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"txid\",\n                        \"type\": \"str or list\",\n                        \"description\": \"transaction id of outputs\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"claim_id\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claim id\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"name\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claim name\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"channel_id\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claims in this channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"not_channel_id\",\n                        \"type\": \"str or list\",\n                        \"description\": \"claims not in this channel\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_spent\",\n                        \"type\": \"bool\",\n                        \"description\": \"only show spent txos\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_not_spent\",\n                        \"type\": \"bool\",\n                        \"description\": \"only show not spent txos\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_my_input_or_output\",\n                        \"type\": \"bool\",\n                        \"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)\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_my_output\",\n                        \"type\": \"bool\",\n                        \"description\": \"show outputs controlled by you\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_not_my_output\",\n                        \"type\": \"bool\",\n                        \"description\": \"show outputs not controlled by you\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_my_input\",\n                        \"type\": \"bool\",\n                        \"description\": \"show outputs created by you\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"is_not_my_input\",\n                        \"type\": \"bool\",\n                        \"description\": \"show outputs not created by you\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"exclude_internal_transfers\",\n                        \"type\": \"bool\",\n                        \"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\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to query\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict results to specific wallet\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"int\",\n                \"examples\": []\n            }\n        ]\n    },\n    \"utxo\": {\n        \"doc\": \"Unspent transaction management.\",\n        \"commands\": [\n            {\n                \"name\": \"utxo_list\",\n                \"description\": \"List unspent transaction outputs\",\n                \"arguments\": [\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to query\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict results to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page\",\n                        \"type\": \"int\",\n                        \"description\": \"page to return during paginating\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page_size\",\n                        \"type\": \"int\",\n                        \"description\": \"number of items on page during pagination\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"utxo_release\",\n                \"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.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"id of the account to query\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"None\",\n                \"examples\": []\n            }\n        ]\n    },\n    \"wallet\": {\n        \"doc\": \"Create, modify and inspect wallets.\",\n        \"commands\": [\n            {\n                \"name\": \"wallet_add\",\n                \"description\": \"Add existing wallet.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"wallet file name\",\n                        \"is_required\": true\n                    }\n                ],\n                \"returns\": \"            {\\n                \\\"id\\\": \\\"wallet_id\\\",\\n                \\\"name\\\": \\\"optional wallet name\\\"\\n            }\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"wallet_balance\",\n                \"description\": \"Return the balance of a wallet\",\n                \"arguments\": [\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"balance for specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"confirmations\",\n                        \"type\": \"int\",\n                        \"description\": \"Only include transactions with this many confirmed blocks.\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(decimal) amount of lbry credits in wallet\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"wallet_create\",\n                \"description\": \"Create a new wallet.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"wallet file name\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"skip_on_startup\",\n                        \"type\": \"bool\",\n                        \"description\": \"don't add wallet to daemon_settings.yml\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"create_account\",\n                        \"type\": \"bool\",\n                        \"description\": \"generates the default account\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"single_key\",\n                        \"type\": \"bool\",\n                        \"description\": \"used with --create_account, creates single-key account\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"            {\\n                \\\"id\\\": \\\"wallet_id\\\",\\n                \\\"name\\\": \\\"optional wallet name\\\"\\n            }\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"wallet_decrypt\",\n                \"description\": \"Decrypt an encrypted wallet, this will remove the wallet password. The wallet must be unlocked to decrypt it\",\n                \"arguments\": [\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(bool) true if wallet is decrypted, otherwise false\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"wallet_encrypt\",\n                \"description\": \"Encrypt an unencrypted wallet with a password\",\n                \"arguments\": [\n                    {\n                        \"name\": \"new_password\",\n                        \"type\": \"str\",\n                        \"description\": \"password to encrypt account\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(bool) true if wallet is decrypted, otherwise false\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"wallet_list\",\n                \"description\": \"List wallets.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"show specific wallet only\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page\",\n                        \"type\": \"int\",\n                        \"description\": \"page to return during paginating\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"page_size\",\n                        \"type\": \"int\",\n                        \"description\": \"number of items on page during pagination\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": [\n                    {\n                        \"title\": \"List your wallets\",\n                        \"curl\": \"curl -d'{\\\"method\\\": \\\"wallet_list\\\", \\\"params\\\": {}}' http://localhost:5279/\",\n                        \"lbrynet\": \"lbrynet wallet list\",\n                        \"python\": \"requests.post(\\\"http://localhost:5279\\\", json={\\\"method\\\": \\\"wallet_list\\\", \\\"params\\\": {}}).json()\",\n                        \"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}\"\n                    }\n                ]\n            },\n            {\n                \"name\": \"wallet_lock\",\n                \"description\": \"Lock an unlocked wallet\",\n                \"arguments\": [\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(bool) true if wallet is locked, otherwise false\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"wallet_reconnect\",\n                \"description\": \"Reconnects ledger network client, applying new configurations.\",\n                \"arguments\": [],\n                \"returns\": \"None\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"wallet_remove\",\n                \"description\": \"Remove an existing wallet.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"name of wallet to remove\",\n                        \"is_required\": true\n                    }\n                ],\n                \"returns\": \"            {\\n                \\\"id\\\": \\\"wallet_id\\\",\\n                \\\"name\\\": \\\"optional wallet name\\\"\\n            }\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"wallet_send\",\n                \"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.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"change_account_id\",\n                        \"type\": \"str\",\n                        \"description\": \"account where change will go\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"funding_account_ids\",\n                        \"type\": \"str\",\n                        \"description\": \"accounts to fund the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"preview\",\n                        \"type\": \"bool\",\n                        \"description\": \"do not broadcast the transaction\",\n                        \"is_required\": false\n                    },\n                    {\n                        \"name\": \"blocking\",\n                        \"type\": \"bool\",\n                        \"description\": \"wait until tx has synced\",\n                        \"is_required\": false\n                    }\n                ],\n                \"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            }\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"wallet_status\",\n                \"description\": \"Status of wallet including encryption/lock state.\",\n                \"arguments\": [\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"status of specific wallet\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"Dictionary of wallet status information.\",\n                \"examples\": []\n            },\n            {\n                \"name\": \"wallet_unlock\",\n                \"description\": \"Unlock an encrypted wallet\",\n                \"arguments\": [\n                    {\n                        \"name\": \"password\",\n                        \"type\": \"str\",\n                        \"description\": \"password to use for unlocking\",\n                        \"is_required\": true\n                    },\n                    {\n                        \"name\": \"wallet_id\",\n                        \"type\": \"str\",\n                        \"description\": \"restrict operation to specific wallet\",\n                        \"is_required\": false\n                    }\n                ],\n                \"returns\": \"(bool) true if wallet is unlocked, otherwise false\",\n                \"examples\": []\n            }\n        ]\n    }\n}"
  },
  {
    "path": "example_daemon_settings.yml",
    "content": "# This is an example daemon_settings.yml file.\n# See https://lbry.tech/resources/daemon-settings for all configuration keys and values\n\nshare_usage_data: True\n\nlbryum_servers:\n  - lbryumx1.lbry.com:50001\n  - lbryumx2.lbry.com:50001\n  - lbryumx4.lbry.com:50001\n\nblockchain_name: lbrycrd_main\n\ndata_dir: /home/lbry/.lbrynet\ndownload_directory: /home/lbry/downloads\n\nsave_blobs: true\nsave_files: false\ndht_node_port: 4444\npeer_port: 3333\nuse_upnp: true\n\n#components_to_skip:\n#  - hash_announcer\n#  - blob_server\n#  - dht\n"
  },
  {
    "path": "lbry/.dockerignore",
    "content": ".git\n.tox\n__pycache__\ndist\nlbry.egg-info\ndocs\ntests\n"
  },
  {
    "path": "lbry/__init__.py",
    "content": "__version__ = \"0.113.0\"\nversion = tuple(map(int, __version__.split('.')))  # pylint: disable=invalid-name\n"
  },
  {
    "path": "lbry/blob/__init__.py",
    "content": "from lbry.utils import get_lbry_hash_obj\n\nMAX_BLOB_SIZE = 2 * 2 ** 20\n\n# digest_size is in bytes, and blob hashes are hex encoded\nBLOBHASH_LENGTH = get_lbry_hash_obj().digest_size * 2\n"
  },
  {
    "path": "lbry/blob/blob_file.py",
    "content": "import os\nimport re\nimport time\nimport asyncio\nimport binascii\nimport logging\nimport typing\nimport contextlib\nfrom io import BytesIO\nfrom cryptography.hazmat.primitives.ciphers import Cipher, modes\nfrom cryptography.hazmat.primitives.ciphers.algorithms import AES\nfrom cryptography.hazmat.primitives.padding import PKCS7\nfrom cryptography.hazmat.backends import default_backend\n\nfrom lbry.utils import get_lbry_hash_obj\nfrom lbry.error import DownloadCancelledError, InvalidBlobHashError, InvalidDataError\n\nfrom lbry.blob import MAX_BLOB_SIZE, BLOBHASH_LENGTH\nfrom lbry.blob.blob_info import BlobInfo\nfrom lbry.blob.writer import HashBlobWriter\n\nlog = logging.getLogger(__name__)\n\n\nHEXMATCH = re.compile(\"^[a-f,0-9]+$\")\nBACKEND = default_backend()\n\n\ndef is_valid_blobhash(blobhash: str) -> bool:\n    \"\"\"Checks whether the blobhash is the correct length and contains only\n    valid characters (0-9, a-f)\n\n    @param blobhash: string, the blobhash to check\n\n    @return: True/False\n    \"\"\"\n    return len(blobhash) == BLOBHASH_LENGTH and HEXMATCH.match(blobhash)\n\n\ndef encrypt_blob_bytes(key: bytes, iv: bytes, unencrypted: bytes) -> typing.Tuple[bytes, str]:\n    cipher = Cipher(AES(key), modes.CBC(iv), backend=BACKEND)\n    padder = PKCS7(AES.block_size).padder()\n    encryptor = cipher.encryptor()\n    encrypted = encryptor.update(padder.update(unencrypted) + padder.finalize()) + encryptor.finalize()\n    digest = get_lbry_hash_obj()\n    digest.update(encrypted)\n    return encrypted, digest.hexdigest()\n\n\ndef decrypt_blob_bytes(data: bytes, length: int, key: bytes, iv: bytes) -> bytes:\n    if len(data) != length:\n        raise ValueError(\"unexpected length\")\n    cipher = Cipher(AES(key), modes.CBC(iv), backend=BACKEND)\n    unpadder = PKCS7(AES.block_size).unpadder()\n    decryptor = cipher.decryptor()\n    return unpadder.update(decryptor.update(data) + decryptor.finalize()) + unpadder.finalize()\n\n\nclass AbstractBlob:\n    \"\"\"\n    A chunk of data (up to 2MB) available on the network which is specified by a sha384 hash\n\n    This class is non-io specific\n    \"\"\"\n    __slots__ = [\n        'loop',\n        'blob_hash',\n        'length',\n        'blob_completed_callback',\n        'blob_directory',\n        'writers',\n        'verified',\n        'writing',\n        'readers',\n        'added_on',\n        'is_mine',\n    ]\n\n    def __init__(\n        self, loop: asyncio.AbstractEventLoop, blob_hash: str, length: typing.Optional[int] = None,\n        blob_completed_callback: typing.Optional[typing.Callable[['AbstractBlob'], asyncio.Task]] = None,\n        blob_directory: typing.Optional[str] = None, added_on: typing.Optional[int] = None, is_mine: bool = False,\n    ):\n        self.loop = loop\n        self.blob_hash = blob_hash\n        self.length = length\n        self.blob_completed_callback = blob_completed_callback\n        self.blob_directory = blob_directory\n        self.writers: typing.Dict[typing.Tuple[typing.Optional[str], typing.Optional[int]], HashBlobWriter] = {}\n        self.verified: asyncio.Event = asyncio.Event()\n        self.writing: asyncio.Event = asyncio.Event()\n        self.readers: typing.List[typing.BinaryIO] = []\n        self.added_on = added_on or time.time()\n        self.is_mine = is_mine\n\n        if not is_valid_blobhash(blob_hash):\n            raise InvalidBlobHashError(blob_hash)\n\n    def __del__(self):\n        if self.writers or self.readers:\n            log.warning(\"%s not closed before being garbage collected\", self.blob_hash)\n            self.close()\n\n    @contextlib.contextmanager\n    def _reader_context(self) -> typing.ContextManager[typing.BinaryIO]:\n        raise NotImplementedError()\n\n    @contextlib.contextmanager\n    def reader_context(self) -> typing.ContextManager[typing.BinaryIO]:\n        if not self.is_readable():\n            raise OSError(f\"{str(type(self))} not readable, {len(self.readers)} readers {len(self.writers)} writers\")\n        with self._reader_context() as reader:\n            try:\n                self.readers.append(reader)\n                yield reader\n            finally:\n                if reader in self.readers:\n                    self.readers.remove(reader)\n\n    def _write_blob(self, blob_bytes: bytes) -> asyncio.Task:\n        raise NotImplementedError()\n\n    def set_length(self, length) -> None:\n        if self.length is not None and length == self.length:\n            return\n        if self.length is None and 0 <= length <= MAX_BLOB_SIZE:\n            self.length = length\n            return\n        log.warning(\"Got an invalid length. Previous length: %s, Invalid length: %s\", self.length, length)\n\n    def get_length(self) -> typing.Optional[int]:\n        return self.length\n\n    def get_is_verified(self) -> bool:\n        return self.verified.is_set()\n\n    def is_readable(self) -> bool:\n        return self.verified.is_set()\n\n    def is_writeable(self) -> bool:\n        return not self.writing.is_set()\n\n    def write_blob(self, blob_bytes: bytes):\n        if not self.is_writeable():\n            raise OSError(\"cannot open blob for writing\")\n        try:\n            self.writing.set()\n            self._write_blob(blob_bytes)\n        finally:\n            self.writing.clear()\n\n    def close(self):\n        while self.writers:\n            _, writer = self.writers.popitem()\n            if writer and writer.finished and not writer.finished.done() and not self.loop.is_closed():\n                writer.finished.cancel()\n        while self.readers:\n            reader = self.readers.pop()\n            if reader:\n                reader.close()\n\n    def delete(self):\n        self.close()\n        self.verified.clear()\n        self.length = None\n\n    async def sendfile(self, writer: asyncio.StreamWriter) -> int:\n        \"\"\"\n        Read and send the file to the writer and return the number of bytes sent\n        \"\"\"\n\n        if not self.is_readable():\n            raise OSError('blob files cannot be read')\n        with self.reader_context() as handle:\n            try:\n                return await self.loop.sendfile(writer.transport, handle, count=self.get_length())\n            except (ConnectionError, BrokenPipeError, RuntimeError, OSError, AttributeError):\n                return -1\n\n    def decrypt(self, key: bytes, iv: bytes) -> bytes:\n        \"\"\"\n        Decrypt a BlobFile to plaintext bytes\n        \"\"\"\n\n        with self.reader_context() as reader:\n            return decrypt_blob_bytes(reader.read(), self.length, key, iv)\n\n    @classmethod\n    async def create_from_unencrypted(\n        cls, loop: asyncio.AbstractEventLoop, blob_dir: typing.Optional[str], key: bytes, iv: bytes,\n        unencrypted: bytes, blob_num: int, added_on: int, is_mine: bool,\n        blob_completed_callback: typing.Optional[typing.Callable[['AbstractBlob'], None]] = None,\n    ) -> BlobInfo:\n        \"\"\"\n        Create an encrypted BlobFile from plaintext bytes\n        \"\"\"\n\n        blob_bytes, blob_hash = encrypt_blob_bytes(key, iv, unencrypted)\n        length = len(blob_bytes)\n        blob = cls(loop, blob_hash, length, blob_completed_callback, blob_dir, added_on, is_mine)\n        writer = blob.get_blob_writer()\n        writer.write(blob_bytes)\n        await blob.verified.wait()\n        return BlobInfo(blob_num, length, binascii.hexlify(iv).decode(), added_on, blob_hash, is_mine)\n\n    def save_verified_blob(self, verified_bytes: bytes):\n        if self.verified.is_set():\n            return\n\n        def update_events(_):\n            self.verified.set()\n            self.writing.clear()\n\n        if self.is_writeable():\n            self.writing.set()\n            task = self._write_blob(verified_bytes)\n            task.add_done_callback(update_events)\n            if self.blob_completed_callback:\n                task.add_done_callback(lambda _: self.blob_completed_callback(self))\n\n    def get_blob_writer(self, peer_address: typing.Optional[str] = None,\n                        peer_port: typing.Optional[int] = None) -> HashBlobWriter:\n        if (peer_address, peer_port) in self.writers and not self.writers[(peer_address, peer_port)].closed():\n            raise OSError(f\"attempted to download blob twice from {peer_address}:{peer_port}\")\n        fut = asyncio.Future()\n        writer = HashBlobWriter(self.blob_hash, self.get_length, fut)\n        self.writers[(peer_address, peer_port)] = writer\n\n        def remove_writer(_):\n            if (peer_address, peer_port) in self.writers:\n                del self.writers[(peer_address, peer_port)]\n\n        fut.add_done_callback(remove_writer)\n\n        def writer_finished_callback(finished: asyncio.Future):\n            try:\n                err = finished.exception()\n                if err:\n                    raise err\n                verified_bytes = finished.result()\n                while self.writers:\n                    _, other = self.writers.popitem()\n                    if other is not writer:\n                        other.close_handle()\n                self.save_verified_blob(verified_bytes)\n            except (InvalidBlobHashError, InvalidDataError) as error:\n                log.warning(\"writer error downloading %s: %s\", self.blob_hash[:8], str(error))\n            except (DownloadCancelledError, asyncio.CancelledError, asyncio.TimeoutError):\n                pass\n\n        fut.add_done_callback(writer_finished_callback)\n        return writer\n\n\nclass BlobBuffer(AbstractBlob):\n    \"\"\"\n    An in-memory only blob\n    \"\"\"\n    def __init__(\n        self, loop: asyncio.AbstractEventLoop, blob_hash: str, length: typing.Optional[int] = None,\n        blob_completed_callback: typing.Optional[typing.Callable[['AbstractBlob'], asyncio.Task]] = None,\n        blob_directory: typing.Optional[str] = None, added_on: typing.Optional[int] = None, is_mine: bool = False\n    ):\n        self._verified_bytes: typing.Optional[BytesIO] = None\n        super().__init__(loop, blob_hash, length, blob_completed_callback, blob_directory, added_on, is_mine)\n\n    @contextlib.contextmanager\n    def _reader_context(self) -> typing.ContextManager[typing.BinaryIO]:\n        if not self.is_readable():\n            raise OSError(\"cannot open blob for reading\")\n        try:\n            yield self._verified_bytes\n        finally:\n            if self._verified_bytes:\n                self._verified_bytes.close()\n            self._verified_bytes = None\n            self.verified.clear()\n\n    def _write_blob(self, blob_bytes: bytes):\n        async def write():\n            if self._verified_bytes:\n                raise OSError(\"already have bytes for blob\")\n            self._verified_bytes = BytesIO(blob_bytes)\n        return self.loop.create_task(write())\n\n    def delete(self):\n        if self._verified_bytes:\n            self._verified_bytes.close()\n            self._verified_bytes = None\n        return super().delete()\n\n    def __del__(self):\n        super().__del__()\n        if self._verified_bytes:\n            self.delete()\n\n\nclass BlobFile(AbstractBlob):\n    \"\"\"\n    A blob existing on the local file system\n    \"\"\"\n    def __init__(\n        self, loop: asyncio.AbstractEventLoop, blob_hash: str, length: typing.Optional[int] = None,\n        blob_completed_callback: typing.Optional[typing.Callable[['AbstractBlob'], asyncio.Task]] = None,\n        blob_directory: typing.Optional[str] = None, added_on: typing.Optional[int] = None, is_mine: bool = False\n    ):\n        super().__init__(loop, blob_hash, length, blob_completed_callback, blob_directory, added_on, is_mine)\n        if not blob_directory or not os.path.isdir(blob_directory):\n            raise OSError(f\"invalid blob directory '{blob_directory}'\")\n        self.file_path = os.path.join(self.blob_directory, self.blob_hash)\n        if self.file_exists:\n            file_size = int(os.stat(self.file_path).st_size)\n            if length and length != file_size:\n                log.warning(\"expected %s to be %s bytes, file has %s\", self.blob_hash, length, file_size)\n                self.delete()\n            else:\n                self.length = file_size\n                self.verified.set()\n\n    @property\n    def file_exists(self):\n        return os.path.isfile(self.file_path)\n\n    def is_writeable(self) -> bool:\n        return super().is_writeable() and not os.path.isfile(self.file_path)\n\n    def get_blob_writer(self, peer_address: typing.Optional[str] = None,\n                        peer_port: typing.Optional[str] = None) -> HashBlobWriter:\n        if self.file_exists:\n            raise OSError(f\"File already exists '{self.file_path}'\")\n        return super().get_blob_writer(peer_address, peer_port)\n\n    @contextlib.contextmanager\n    def _reader_context(self) -> typing.ContextManager[typing.BinaryIO]:\n        handle = open(self.file_path, 'rb')\n        try:\n            yield handle\n        finally:\n            handle.close()\n\n    def _write_blob(self, blob_bytes: bytes):\n        def _write_blob():\n            with open(self.file_path, 'wb') as f:\n                f.write(blob_bytes)\n\n        async def write_blob():\n            await self.loop.run_in_executor(None, _write_blob)\n\n        return self.loop.create_task(write_blob())\n\n    def delete(self):\n        super().delete()\n        if os.path.isfile(self.file_path):\n            os.remove(self.file_path)\n\n    @classmethod\n    async def create_from_unencrypted(\n        cls, loop: asyncio.AbstractEventLoop, blob_dir: typing.Optional[str], key: bytes, iv: bytes,\n        unencrypted: bytes, blob_num: int, added_on: float, is_mine: bool,\n        blob_completed_callback: typing.Optional[typing.Callable[['AbstractBlob'], asyncio.Task]] = None\n    ) -> BlobInfo:\n        if not blob_dir or not os.path.isdir(blob_dir):\n            raise OSError(f\"cannot create blob in directory: '{blob_dir}'\")\n        return await super().create_from_unencrypted(\n            loop, blob_dir, key, iv, unencrypted, blob_num, added_on, is_mine, blob_completed_callback\n        )\n"
  },
  {
    "path": "lbry/blob/blob_info.py",
    "content": "import typing\n\n\nclass BlobInfo:\n    __slots__ = [\n        'blob_hash',\n        'blob_num',\n        'length',\n        'iv',\n        'added_on',\n        'is_mine'\n    ]\n\n    def __init__(\n            self, blob_num: int, length: int, iv: str, added_on,\n             blob_hash: typing.Optional[str] = None, is_mine=False):\n        self.blob_hash = blob_hash\n        self.blob_num = blob_num\n        self.length = length\n        self.iv = iv\n        self.added_on = added_on\n        self.is_mine = is_mine\n\n    def as_dict(self) -> typing.Dict:\n        d = {\n            'length': self.length,\n            'blob_num': self.blob_num,\n            'iv': self.iv,\n        }\n        if self.blob_hash:  # non-terminator blobs have a blob hash\n            d['blob_hash'] = self.blob_hash\n        return d\n"
  },
  {
    "path": "lbry/blob/blob_manager.py",
    "content": "import os\nimport typing\nimport asyncio\nimport logging\nfrom lbry.utils import LRUCacheWithMetrics\nfrom lbry.blob.blob_file import is_valid_blobhash, BlobFile, BlobBuffer, AbstractBlob\nfrom lbry.stream.descriptor import StreamDescriptor\nfrom lbry.connection_manager import ConnectionManager\n\nif typing.TYPE_CHECKING:\n    from lbry.conf import Config\n    from lbry.dht.protocol.data_store import DictDataStore\n    from lbry.extras.daemon.storage import SQLiteStorage\n\nlog = logging.getLogger(__name__)\n\n\nclass BlobManager:\n    def __init__(self, loop: asyncio.AbstractEventLoop, blob_dir: str, storage: 'SQLiteStorage', config: 'Config',\n                 node_data_store: typing.Optional['DictDataStore'] = None):\n        \"\"\"\n        This class stores blobs on the hard disk\n\n        blob_dir - directory where blobs are stored\n        storage - SQLiteStorage object\n        \"\"\"\n        self.loop = loop\n        self.blob_dir = blob_dir\n        self.storage = storage\n        self._node_data_store = node_data_store\n        self.completed_blob_hashes: typing.Set[str] = set() if not self._node_data_store\\\n            else self._node_data_store.completed_blobs\n        self.blobs: typing.Dict[str, AbstractBlob] = {}\n        self.config = config\n        self.decrypted_blob_lru_cache = None if not self.config.blob_lru_cache_size else LRUCacheWithMetrics(\n            self.config.blob_lru_cache_size)\n        self.connection_manager = ConnectionManager(loop)\n\n    def _get_blob(self, blob_hash: str, length: typing.Optional[int] = None, is_mine: bool = False):\n        if self.config.save_blobs or (\n                is_valid_blobhash(blob_hash) and os.path.isfile(os.path.join(self.blob_dir, blob_hash))):\n            return BlobFile(\n                self.loop, blob_hash, length, self.blob_completed, self.blob_dir, is_mine=is_mine\n            )\n        return BlobBuffer(\n            self.loop, blob_hash, length, self.blob_completed, self.blob_dir, is_mine=is_mine\n        )\n\n    def get_blob(self, blob_hash, length: typing.Optional[int] = None, is_mine: bool = False):\n        if blob_hash in self.blobs:\n            if self.config.save_blobs and isinstance(self.blobs[blob_hash], BlobBuffer):\n                buffer = self.blobs.pop(blob_hash)\n                if blob_hash in self.completed_blob_hashes:\n                    self.completed_blob_hashes.remove(blob_hash)\n                self.blobs[blob_hash] = self._get_blob(blob_hash, length, is_mine)\n                if buffer.is_readable():\n                    with buffer.reader_context() as reader:\n                        self.blobs[blob_hash].write_blob(reader.read())\n            if length and self.blobs[blob_hash].length is None:\n                self.blobs[blob_hash].set_length(length)\n        else:\n            self.blobs[blob_hash] = self._get_blob(blob_hash, length, is_mine)\n        return self.blobs[blob_hash]\n\n    def is_blob_verified(self, blob_hash: str, length: typing.Optional[int] = None) -> bool:\n        if not is_valid_blobhash(blob_hash):\n            raise ValueError(blob_hash)\n        if not os.path.isfile(os.path.join(self.blob_dir, blob_hash)):\n            return False\n        if blob_hash in self.blobs:\n            return self.blobs[blob_hash].get_is_verified()\n        return self._get_blob(blob_hash, length).get_is_verified()\n\n    async def setup(self) -> bool:\n        def get_files_in_blob_dir() -> typing.Set[str]:\n            if not self.blob_dir:\n                return set()\n            return {\n                item.name for item in os.scandir(self.blob_dir) if is_valid_blobhash(item.name)\n            }\n\n        in_blobfiles_dir = await self.loop.run_in_executor(None, get_files_in_blob_dir)\n        to_add = await self.storage.sync_missing_blobs(in_blobfiles_dir)\n        if to_add:\n            self.completed_blob_hashes.update(to_add)\n        # check blobs that aren't set as finished but were seen on disk\n        await self.ensure_completed_blobs_status(in_blobfiles_dir - to_add)\n        if self.config.track_bandwidth:\n            self.connection_manager.start()\n        return True\n\n    def stop(self):\n        self.connection_manager.stop()\n        while self.blobs:\n            _, blob = self.blobs.popitem()\n            blob.close()\n        self.completed_blob_hashes.clear()\n\n    def get_stream_descriptor(self, sd_hash):\n        return StreamDescriptor.from_stream_descriptor_blob(self.loop, self.blob_dir, self.get_blob(sd_hash))\n\n    def blob_completed(self, blob: AbstractBlob) -> asyncio.Task:\n        if blob.blob_hash is None:\n            raise Exception(\"Blob hash is None\")\n        if not blob.length:\n            raise Exception(\"Blob has a length of 0\")\n        if isinstance(blob, BlobFile):\n            if blob.blob_hash not in self.completed_blob_hashes:\n                self.completed_blob_hashes.add(blob.blob_hash)\n            return self.loop.create_task(self.storage.add_blobs(\n                (blob.blob_hash, blob.length, blob.added_on, blob.is_mine), finished=True)\n            )\n        else:\n            return self.loop.create_task(self.storage.add_blobs(\n                (blob.blob_hash, blob.length, blob.added_on, blob.is_mine), finished=False)\n            )\n\n    async def ensure_completed_blobs_status(self, blob_hashes: typing.Iterable[str]):\n        \"\"\"Ensures that completed blobs from a given list of blob hashes are set as 'finished' in the database.\"\"\"\n        to_add = []\n        for blob_hash in blob_hashes:\n            if not self.is_blob_verified(blob_hash):\n                continue\n            blob = self.get_blob(blob_hash)\n            to_add.append((blob.blob_hash, blob.length, blob.added_on, blob.is_mine))\n            if len(to_add) > 500:\n                await self.storage.add_blobs(*to_add, finished=True)\n                to_add.clear()\n        return await self.storage.add_blobs(*to_add, finished=True)\n\n    def delete_blob(self, blob_hash: str):\n        if not is_valid_blobhash(blob_hash):\n            raise Exception(\"invalid blob hash to delete\")\n\n        if blob_hash not in self.blobs:\n            if self.blob_dir and os.path.isfile(os.path.join(self.blob_dir, blob_hash)):\n                os.remove(os.path.join(self.blob_dir, blob_hash))\n        else:\n            self.blobs.pop(blob_hash).delete()\n            if blob_hash in self.completed_blob_hashes:\n                self.completed_blob_hashes.remove(blob_hash)\n\n    async def delete_blobs(self, blob_hashes: typing.List[str], delete_from_db: typing.Optional[bool] = True):\n        for blob_hash in blob_hashes:\n            self.delete_blob(blob_hash)\n\n        if delete_from_db:\n            await self.storage.delete_blobs_from_db(blob_hashes)\n"
  },
  {
    "path": "lbry/blob/disk_space_manager.py",
    "content": "import asyncio\nimport logging\n\nlog = logging.getLogger(__name__)\n\n\nclass DiskSpaceManager:\n\n    def __init__(self, config, db, blob_manager, cleaning_interval=30 * 60, analytics=None):\n        self.config = config\n        self.db = db\n        self.blob_manager = blob_manager\n        self.cleaning_interval = cleaning_interval\n        self.running = False\n        self.task = None\n        self.analytics = analytics\n        self._used_space_bytes = None\n\n    async def get_free_space_mb(self, is_network_blob=False):\n        limit_mb = self.config.network_storage_limit if is_network_blob else self.config.blob_storage_limit\n        space_used_mb = await self.get_space_used_mb()\n        space_used_mb = space_used_mb['network_storage'] if is_network_blob else space_used_mb['content_storage']\n        return max(0, limit_mb - space_used_mb)\n\n    async def get_space_used_bytes(self):\n        self._used_space_bytes = await self.db.get_stored_blob_disk_usage()\n        return self._used_space_bytes\n\n    async def get_space_used_mb(self, cached=True):\n        cached = cached and self._used_space_bytes is not None\n        space_used_bytes = self._used_space_bytes if cached else await self.get_space_used_bytes()\n        return {key: int(value/1024.0/1024.0) for key, value in space_used_bytes.items()}\n\n    async def clean(self):\n        await self._clean(False)\n        await self._clean(True)\n\n    async def _clean(self, is_network_blob=False):\n        space_used_mb = await self.get_space_used_mb(cached=False)\n        if is_network_blob:\n            space_used_mb = space_used_mb['network_storage']\n        else:\n            space_used_mb = space_used_mb['content_storage'] + space_used_mb['private_storage']\n        storage_limit_mb = self.config.network_storage_limit if is_network_blob else self.config.blob_storage_limit\n        if self.analytics:\n            asyncio.create_task(\n                self.analytics.send_disk_space_used(space_used_mb, storage_limit_mb, is_network_blob)\n            )\n        delete = []\n        available = storage_limit_mb - space_used_mb\n        if storage_limit_mb == 0 if not is_network_blob else available >= 0:\n            return 0\n        for blob_hash, file_size, _ in await self.db.get_stored_blobs(is_mine=False, is_network_blob=is_network_blob):\n            delete.append(blob_hash)\n            available += int(file_size/1024.0/1024.0)\n            if available >= 0:\n                break\n        if delete:\n            await self.db.stop_all_files()\n            await self.blob_manager.delete_blobs(delete, delete_from_db=True)\n        self._used_space_bytes = None\n        return len(delete)\n\n    async def cleaning_loop(self):\n        while self.running:\n            await asyncio.sleep(self.cleaning_interval)\n            await self.clean()\n\n    async def start(self):\n        self.running = True\n        self.task = asyncio.create_task(self.cleaning_loop())\n        self.task.add_done_callback(lambda _: log.info(\"Stopping blob cleanup service.\"))\n\n    async def stop(self):\n        if self.running:\n            self.running = False\n            self.task.cancel()\n"
  },
  {
    "path": "lbry/blob/writer.py",
    "content": "import typing\nimport logging\nimport asyncio\nfrom io import BytesIO\nfrom lbry.error import InvalidBlobHashError, InvalidDataError\nfrom lbry.utils import get_lbry_hash_obj\n\nlog = logging.getLogger(__name__)\n\n\nclass HashBlobWriter:\n    def __init__(self, expected_blob_hash: str, get_length: typing.Callable[[], int],\n                 finished: asyncio.Future):\n        self.expected_blob_hash = expected_blob_hash\n        self.get_length = get_length\n        self.buffer = BytesIO()\n        self.finished = finished\n        self.finished.add_done_callback(lambda *_: self.close_handle())\n        self._hashsum = get_lbry_hash_obj()\n        self.len_so_far = 0\n\n    def __del__(self):\n        if self.buffer is not None:\n            log.warning(\"Garbage collection was called, but writer was not closed yet\")\n            self.close_handle()\n\n    def calculate_blob_hash(self) -> str:\n        return self._hashsum.hexdigest()\n\n    def closed(self):\n        return self.buffer is None or self.buffer.closed\n\n    def write(self, data: bytes):\n        expected_length = self.get_length()\n        if not expected_length:\n            raise OSError(\"unknown blob length\")\n        if self.buffer is None:\n            log.warning(\"writer has already been closed\")\n            if not self.finished.done():\n                self.finished.cancel()\n                return\n            raise OSError('I/O operation on closed file')\n\n        self._hashsum.update(data)\n        self.len_so_far += len(data)\n        if self.len_so_far > expected_length:\n            self.finished.set_exception(InvalidDataError(\n                f'Length so far is greater than the expected length. {self.len_so_far} to {expected_length}'\n            ))\n            self.close_handle()\n            return\n        self.buffer.write(data)\n        if self.len_so_far == expected_length:\n            blob_hash = self.calculate_blob_hash()\n            if blob_hash != self.expected_blob_hash:\n                self.finished.set_exception(InvalidBlobHashError(\n                    f\"blob hash is {blob_hash} vs expected {self.expected_blob_hash}\"\n                ))\n            elif self.finished and not (self.finished.done() or self.finished.cancelled()):\n                self.finished.set_result(self.buffer.getvalue())\n            self.close_handle()\n\n    def close_handle(self):\n        if not self.finished.done():\n            self.finished.cancel()\n        if self.buffer is not None:\n            self.buffer.close()\n            self.buffer = None\n"
  },
  {
    "path": "lbry/blob_exchange/__init__.py",
    "content": ""
  },
  {
    "path": "lbry/blob_exchange/client.py",
    "content": "import asyncio\nimport time\nimport logging\nimport typing\nimport binascii\nfrom typing import Optional\nfrom lbry.error import InvalidBlobHashError, InvalidDataError\nfrom lbry.blob_exchange.serialization import BlobResponse, BlobRequest\nfrom lbry.utils import cache_concurrent\nif typing.TYPE_CHECKING:\n    from lbry.blob.blob_file import AbstractBlob\n    from lbry.blob.writer import HashBlobWriter\n    from lbry.connection_manager import ConnectionManager\n\nlog = logging.getLogger(__name__)\n\n\nclass BlobExchangeClientProtocol(asyncio.Protocol):\n    def __init__(self, loop: asyncio.AbstractEventLoop, peer_timeout: typing.Optional[float] = 10,\n                 connection_manager: typing.Optional['ConnectionManager'] = None):\n        self.loop = loop\n        self.peer_port: typing.Optional[int] = None\n        self.peer_address: typing.Optional[str] = None\n        self.transport: typing.Optional[asyncio.Transport] = None\n        self.peer_timeout = peer_timeout\n        self.connection_manager = connection_manager\n        self.writer: typing.Optional['HashBlobWriter'] = None\n        self.blob: typing.Optional['AbstractBlob'] = None\n\n        self._blob_bytes_received = 0\n        self._response_fut: typing.Optional[asyncio.Future] = None\n        self.buf = b''\n\n        # this is here to handle the race when the downloader is closed right as response_fut gets a result\n        self.closed = asyncio.Event()\n\n    def data_received(self, data: bytes):\n        if self.connection_manager:\n            if not self.peer_address:\n                addr_info = self.transport.get_extra_info('peername')\n                self.peer_address, self.peer_port = addr_info\n            # assert self.peer_address is not None\n            self.connection_manager.received_data(f\"{self.peer_address}:{self.peer_port}\", len(data))\n        if not self.transport or self.transport.is_closing():\n            log.warning(\"transport closing, but got more bytes from %s:%i\\n%s\", self.peer_address, self.peer_port,\n                        binascii.hexlify(data))\n            if self._response_fut and not self._response_fut.done():\n                self._response_fut.cancel()\n            return\n        if not self._response_fut:\n            log.warning(\"Protocol received data before expected, probable race on keep alive. Closing transport.\")\n            return self.close()\n        if self._blob_bytes_received and not self.writer.closed():\n            return self._write(data)\n\n        response = BlobResponse.deserialize(self.buf + data)\n        if not response.responses and not self._response_fut.done():\n            self.buf += data\n            return\n        else:\n            self.buf = b''\n\n        if response.responses and self.blob:\n            blob_response = response.get_blob_response()\n            if blob_response and not blob_response.error and blob_response.blob_hash == self.blob.blob_hash:\n                # set the expected length for the incoming blob if we didn't know it\n                self.blob.set_length(blob_response.length)\n            elif blob_response and not blob_response.error and self.blob.blob_hash != blob_response.blob_hash:\n                # the server started sending a blob we didn't request\n                log.warning(\"%s started sending blob we didn't request %s instead of %s\", self.peer_address,\n                            blob_response.blob_hash, self.blob.blob_hash)\n                return\n        if response.responses:\n            log.debug(\"got response from %s:%i <- %s\", self.peer_address, self.peer_port, response.to_dict())\n            # fire the Future with the response to our request\n            self._response_fut.set_result(response)\n        if response.blob_data and self.writer and not self.writer.closed():\n            # log.debug(\"got %i blob bytes from %s:%i\", len(response.blob_data), self.peer_address, self.peer_port)\n            # write blob bytes if we're writing a blob and have blob bytes to write\n            self._write(response.blob_data)\n\n    def _write(self, data: bytes):\n        if len(data) > (self.blob.get_length() - self._blob_bytes_received):\n            data = data[:(self.blob.get_length() - self._blob_bytes_received)]\n            log.warning(\"got more than asked from %s:%d, probable sendfile bug\", self.peer_address, self.peer_port)\n        self._blob_bytes_received += len(data)\n        try:\n            self.writer.write(data)\n        except OSError as err:\n            log.error(\"error downloading blob from %s:%i: %s\", self.peer_address, self.peer_port, err)\n            if self._response_fut and not self._response_fut.done():\n                self._response_fut.set_exception(err)\n        except asyncio.TimeoutError as err:\n            log.error(\"%s downloading blob from %s:%i\", str(err), self.peer_address, self.peer_port)\n            if self._response_fut and not self._response_fut.done():\n                self._response_fut.set_exception(err)\n\n    async def _download_blob(self) -> typing.Tuple[int, Optional['BlobExchangeClientProtocol']]:  # pylint: disable=too-many-return-statements\n        \"\"\"\n        :return: download success (bool), connected protocol (BlobExchangeClientProtocol)\n        \"\"\"\n        start_time = time.perf_counter()\n        request = BlobRequest.make_request_for_blob_hash(self.blob.blob_hash)\n        blob_hash = self.blob.blob_hash\n        if not self.peer_address:\n            addr_info = self.transport.get_extra_info('peername')\n            self.peer_address, self.peer_port = addr_info\n        try:\n            msg = request.serialize()\n            log.debug(\"send request to %s:%i -> %s\", self.peer_address, self.peer_port, msg.decode())\n            self.transport.write(msg)\n            if self.connection_manager:\n                self.connection_manager.sent_data(f\"{self.peer_address}:{self.peer_port}\", len(msg))\n            response: BlobResponse = await asyncio.wait_for(self._response_fut, self.peer_timeout)\n            availability_response = response.get_availability_response()\n            price_response = response.get_price_response()\n            blob_response = response.get_blob_response()\n            if self.closed.is_set():\n                msg = f\"cancelled blob request for {blob_hash} immediately after we got a response\"\n                log.warning(msg)\n                raise asyncio.CancelledError(msg)\n            if (not blob_response or blob_response.error) and\\\n                    (not availability_response or not availability_response.available_blobs):\n                log.warning(\"%s not in availability response from %s:%i\", self.blob.blob_hash, self.peer_address,\n                            self.peer_port)\n                log.warning(response.to_dict())\n                return self._blob_bytes_received, self.close()\n            elif availability_response and availability_response.available_blobs and \\\n                    availability_response.available_blobs != [self.blob.blob_hash]:\n                log.warning(\"blob availability response doesn't match our request from %s:%i\",\n                            self.peer_address, self.peer_port)\n                return self._blob_bytes_received, self.close()\n            elif not availability_response:\n                log.warning(\"response from %s:%i did not include an availability response (we requested %s)\",\n                            self.peer_address, self.peer_port, blob_hash)\n                return self._blob_bytes_received, self.close()\n\n            if not price_response or price_response.blob_data_payment_rate != 'RATE_ACCEPTED':\n                log.warning(\"data rate rejected by %s:%i\", self.peer_address, self.peer_port)\n                return self._blob_bytes_received, self.close()\n            if not blob_response or blob_response.error:\n                log.warning(\"blob can't be downloaded from %s:%i\", self.peer_address, self.peer_port)\n                return self._blob_bytes_received, self.close()\n            if not blob_response.error and blob_response.blob_hash != self.blob.blob_hash:\n                log.warning(\"incoming blob hash mismatch from %s:%i\", self.peer_address, self.peer_port)\n                return self._blob_bytes_received, self.close()\n            if self.blob.length is not None and self.blob.length != blob_response.length:\n                log.warning(\"incoming blob unexpected length from %s:%i\", self.peer_address, self.peer_port)\n                return self._blob_bytes_received, self.close()\n            msg = f\"downloading {self.blob.blob_hash[:8]} from {self.peer_address}:{self.peer_port},\" \\\n                f\" timeout in {self.peer_timeout}\"\n            log.debug(msg)\n            msg = f\"downloaded {self.blob.blob_hash[:8]} from {self.peer_address}:{self.peer_port}\"\n            await asyncio.wait_for(self.writer.finished, self.peer_timeout)\n            # wait for the io to finish\n            await self.blob.verified.wait()\n            log.info(\"%s at %fMB/s\", msg,\n                     round((float(self._blob_bytes_received) /\n                            float(time.perf_counter() - start_time)) / 1000000.0, 2))\n            # await self.blob.finished_writing.wait()  not necessary, but a dangerous change. TODO: is it needed?\n            return self._blob_bytes_received, self\n        except asyncio.TimeoutError:\n            return self._blob_bytes_received, self.close()\n        except (InvalidBlobHashError, InvalidDataError):\n            log.warning(\"invalid blob from %s:%i\", self.peer_address, self.peer_port)\n            return self._blob_bytes_received, self.close()\n\n    def close(self):\n        self.closed.set()\n        if self._response_fut and not self._response_fut.done():\n            self._response_fut.cancel()\n        if self.writer and not self.writer.closed():\n            self.writer.close_handle()\n        self._response_fut = None\n        self.writer = None\n        self.blob = None\n        if self.transport:\n            self.transport.close()\n        self.transport = None\n        self.buf = b''\n\n    async def download_blob(self, blob: 'AbstractBlob') -> typing.Tuple[int, Optional['BlobExchangeClientProtocol']]:\n        self.closed.clear()\n        blob_hash = blob.blob_hash\n        if blob.get_is_verified() or not blob.is_writeable():\n            return 0, self\n        try:\n            self._blob_bytes_received = 0\n            self.blob, self.writer = blob, blob.get_blob_writer(self.peer_address, self.peer_port)\n            self._response_fut = asyncio.Future()\n            return await self._download_blob()\n        except OSError:\n            # i'm not sure how to fix this race condition - jack\n            log.warning(\"race happened downloading %s from %s:%s\", blob_hash, self.peer_address, self.peer_port)\n            # return self._blob_bytes_received, self.transport\n            raise\n        except asyncio.TimeoutError:\n            if self._response_fut and not self._response_fut.done():\n                self._response_fut.cancel()\n            self.close()\n            return self._blob_bytes_received, None\n        except asyncio.CancelledError:\n            self.close()\n            raise\n        finally:\n            if self.writer and not self.writer.closed():\n                self.writer.close_handle()\n                self.writer = None\n\n    def connection_made(self, transport: asyncio.Transport):\n        addr = transport.get_extra_info('peername')\n        self.peer_address, self.peer_port = addr[0], addr[1]\n        self.transport = transport\n        if self.connection_manager:\n            self.connection_manager.connection_made(f\"{self.peer_address}:{self.peer_port}\")\n        log.debug(\"connection made to %s:%i\", self.peer_address, self.peer_port)\n\n    def connection_lost(self, exc):\n        if self.connection_manager:\n            self.connection_manager.outgoing_connection_lost(f\"{self.peer_address}:{self.peer_port}\")\n        log.debug(\"connection lost to %s:%i (reason: %s, %s)\", self.peer_address, self.peer_port, str(exc),\n                  str(type(exc)))\n        self.close()\n\n\n@cache_concurrent\nasync def request_blob(loop: asyncio.AbstractEventLoop, blob: Optional['AbstractBlob'], address: str,\n                       tcp_port: int, peer_connect_timeout: float, blob_download_timeout: float,\n                       connected_protocol: Optional['BlobExchangeClientProtocol'] = None,\n                       connection_id: int = 0, connection_manager: Optional['ConnectionManager'] = None)\\\n        -> typing.Tuple[int, Optional['BlobExchangeClientProtocol']]:\n    \"\"\"\n    Returns [<amount of bytes received>, <client protocol if connected>]\n    \"\"\"\n\n    protocol = connected_protocol\n    if not connected_protocol or not connected_protocol.transport or connected_protocol.transport.is_closing():\n        connected_protocol = None\n        protocol = BlobExchangeClientProtocol(\n            loop, blob_download_timeout, connection_manager\n        )\n    else:\n        log.debug(\"reusing connection for %s:%d\", address, tcp_port)\n    try:\n        if not connected_protocol:\n            await asyncio.wait_for(loop.create_connection(lambda: protocol, address, tcp_port),\n                                   peer_connect_timeout)\n            connected_protocol = protocol\n        if blob is None or blob.get_is_verified() or not blob.is_writeable():\n            # blob is None happens when we are just opening a connection\n            # file exists but not verified means someone is writing right now, give it time, come back later\n            return 0, connected_protocol\n        return await connected_protocol.download_blob(blob)\n    except (asyncio.TimeoutError, ConnectionRefusedError, ConnectionAbortedError, OSError):\n        return 0, None\n"
  },
  {
    "path": "lbry/blob_exchange/downloader.py",
    "content": "import asyncio\nimport typing\nimport logging\nfrom lbry.utils import cache_concurrent\nfrom lbry.blob_exchange.client import request_blob\nfrom lbry.dht.node import get_kademlia_peers_from_hosts\nif typing.TYPE_CHECKING:\n    from lbry.conf import Config\n    from lbry.dht.node import Node\n    from lbry.dht.peer import KademliaPeer\n    from lbry.blob.blob_manager import BlobManager\n    from lbry.blob.blob_file import AbstractBlob\n    from lbry.blob_exchange.client import BlobExchangeClientProtocol\n\nlog = logging.getLogger(__name__)\n\n\nclass BlobDownloader:\n    BAN_FACTOR = 2.0  # fixme: when connection manager gets implemented, move it out from here\n\n    def __init__(self, loop: asyncio.AbstractEventLoop, config: 'Config', blob_manager: 'BlobManager',\n                 peer_queue: asyncio.Queue):\n        self.loop = loop\n        self.config = config\n        self.blob_manager = blob_manager\n        self.peer_queue = peer_queue\n        self.active_connections: typing.Dict['KademliaPeer', asyncio.Task] = {}  # active request_blob calls\n        self.ignored: typing.Dict['KademliaPeer', int] = {}\n        self.scores: typing.Dict['KademliaPeer', int] = {}\n        self.failures: typing.Dict['KademliaPeer', int] = {}\n        self.connection_failures: typing.Set['KademliaPeer'] = set()\n        self.connections: typing.Dict['KademliaPeer', 'BlobExchangeClientProtocol'] = {}\n        self.is_running = asyncio.Event()\n\n    def should_race_continue(self, blob: 'AbstractBlob'):\n        max_probes = self.config.max_connections_per_download * (1 if self.connections else 10)\n        if len(self.active_connections) >= max_probes:\n            return False\n        return not (blob.get_is_verified() or not blob.is_writeable())\n\n    async def request_blob_from_peer(self, blob: 'AbstractBlob', peer: 'KademliaPeer', connection_id: int = 0,\n                                     just_probe: bool = False):\n        if blob.get_is_verified():\n            return\n        start = self.loop.time()\n        bytes_received, protocol = await request_blob(\n            self.loop, blob if not just_probe else None, peer.address, peer.tcp_port, self.config.peer_connect_timeout,\n            self.config.blob_download_timeout, connected_protocol=self.connections.get(peer),\n            connection_id=connection_id, connection_manager=self.blob_manager.connection_manager\n        )\n        if not bytes_received and not protocol and peer not in self.connection_failures:\n            self.connection_failures.add(peer)\n        if not protocol and peer not in self.ignored:\n            self.ignored[peer] = self.loop.time()\n            log.debug(\"drop peer %s:%i\", peer.address, peer.tcp_port)\n            self.failures[peer] = self.failures.get(peer, 0) + 1\n            if peer in self.connections:\n                del self.connections[peer]\n        elif protocol:\n            log.debug(\"keep peer %s:%i\", peer.address, peer.tcp_port)\n            self.failures[peer] = 0\n            self.connections[peer] = protocol\n            elapsed = self.loop.time() - start\n            self.scores[peer] = bytes_received / elapsed if bytes_received and elapsed else 1\n\n    async def new_peer_or_finished(self):\n        active_tasks = list(self.active_connections.values()) + [asyncio.create_task(asyncio.sleep(1))]\n        await asyncio.wait(active_tasks, return_when='FIRST_COMPLETED')\n\n    def cleanup_active(self):\n        if not self.active_connections and not self.connections:\n            self.clearbanned()\n        to_remove = [peer for (peer, task) in self.active_connections.items() if task.done()]\n        for peer in to_remove:\n            del self.active_connections[peer]\n\n    def clearbanned(self):\n        now = self.loop.time()\n        self.ignored = {\n            peer: when for (peer, when) in self.ignored.items()\n            if (now - when) < min(30.0, (self.failures.get(peer, 0) ** self.BAN_FACTOR))\n        }\n\n    @cache_concurrent\n    async def download_blob(self, blob_hash: str, length: typing.Optional[int] = None,\n                            connection_id: int = 0) -> 'AbstractBlob':\n        blob = self.blob_manager.get_blob(blob_hash, length)\n        if blob.get_is_verified():\n            return blob\n        self.is_running.set()\n        try:\n            while not blob.get_is_verified() and self.is_running.is_set():\n                batch: typing.Set['KademliaPeer'] = set(self.connections.keys())\n                while not self.peer_queue.empty():\n                    batch.update(self.peer_queue.get_nowait())\n                log.debug(\n                    \"%s running, %d peers, %d ignored, %d active, %s connections\", blob_hash[:6],\n                    len(batch), len(self.ignored), len(self.active_connections), len(self.connections)\n                )\n                for peer in sorted(batch, key=lambda peer: self.scores.get(peer, 0), reverse=True):\n                    if peer in self.ignored:\n                        continue\n                    if peer in self.active_connections or not self.should_race_continue(blob):\n                        continue\n                    log.debug(\"request %s from %s:%i\", blob_hash[:8], peer.address, peer.tcp_port)\n                    t = self.loop.create_task(self.request_blob_from_peer(blob, peer, connection_id))\n                    self.active_connections[peer] = t\n                self.peer_queue.put_nowait(list(batch))\n                await self.new_peer_or_finished()\n                self.cleanup_active()\n            log.debug(\"downloaded %s\", blob_hash[:8])\n            return blob\n        finally:\n            blob.close()\n            if self.loop.is_running():\n                self.loop.call_soon(self.cleanup_active)\n\n    def close(self):\n        self.connection_failures.clear()\n        self.scores.clear()\n        self.ignored.clear()\n        self.is_running.clear()\n        for protocol in self.connections.values():\n            protocol.close()\n\n\nasync def download_blob(loop, config: 'Config', blob_manager: 'BlobManager', dht_node: 'Node',\n                        blob_hash: str) -> 'AbstractBlob':\n    search_queue = asyncio.Queue(maxsize=config.max_connections_per_download)\n    search_queue.put_nowait(blob_hash)\n    peer_queue, accumulate_task = dht_node.accumulate_peers(search_queue)\n    fixed_peers = None if not config.fixed_peers else await get_kademlia_peers_from_hosts(config.fixed_peers)\n    if fixed_peers:\n        loop.call_later(config.fixed_peer_delay, peer_queue.put_nowait, fixed_peers)\n    downloader = BlobDownloader(loop, config, blob_manager, peer_queue)\n    try:\n        return await downloader.download_blob(blob_hash)\n    finally:\n        if accumulate_task and not accumulate_task.done():\n            accumulate_task.cancel()\n        downloader.close()\n"
  },
  {
    "path": "lbry/blob_exchange/serialization.py",
    "content": "import typing\nimport json\nimport logging\n\nlog = logging.getLogger(__name__)\n\n\nclass BlobMessage:\n    key = ''\n\n    def to_dict(self) -> typing.Dict:\n        raise NotImplementedError()\n\n\nclass BlobPriceRequest(BlobMessage):\n    key = 'blob_data_payment_rate'\n\n    def __init__(self, blob_data_payment_rate: float, **kwargs) -> None:\n        self.blob_data_payment_rate = blob_data_payment_rate\n\n    def to_dict(self) -> typing.Dict:\n        return {\n            self.key: self.blob_data_payment_rate\n        }\n\n\nclass BlobPriceResponse(BlobMessage):\n    key = 'blob_data_payment_rate'\n    rate_accepted = 'RATE_ACCEPTED'\n    rate_too_low = 'RATE_TOO_LOW'\n    rate_unset = 'RATE_UNSET'\n\n    def __init__(self, blob_data_payment_rate: str, **kwargs) -> None:\n        if blob_data_payment_rate not in (self.rate_accepted, self.rate_too_low, self.rate_unset):\n            raise ValueError(blob_data_payment_rate)\n        self.blob_data_payment_rate = blob_data_payment_rate\n\n    def to_dict(self) -> typing.Dict:\n        return {\n            self.key: self.blob_data_payment_rate\n        }\n\n\nclass BlobAvailabilityRequest(BlobMessage):\n    key = 'requested_blobs'\n\n    def __init__(self, requested_blobs: typing.List[str], lbrycrd_address: typing.Optional[bool] = True,\n                 **kwargs) -> None:\n        assert len(requested_blobs) > 0\n        self.requested_blobs = requested_blobs\n        self.lbrycrd_address = lbrycrd_address\n\n    def to_dict(self) -> typing.Dict:\n        return {\n            self.key: self.requested_blobs,\n            'lbrycrd_address': self.lbrycrd_address\n        }\n\n\nclass BlobAvailabilityResponse(BlobMessage):\n    key = 'available_blobs'\n\n    def __init__(self, available_blobs: typing.List[str], lbrycrd_address: typing.Optional[str] = True,\n                 **kwargs) -> None:\n        self.available_blobs = available_blobs\n        self.lbrycrd_address = lbrycrd_address\n\n    def to_dict(self) -> typing.Dict:\n        d = {\n            self.key: self.available_blobs\n        }\n        if self.lbrycrd_address:\n            d['lbrycrd_address'] = self.lbrycrd_address\n        return d\n\n\nclass BlobDownloadRequest(BlobMessage):\n    key = 'requested_blob'\n\n    def __init__(self, requested_blob: str, **kwargs) -> None:\n        self.requested_blob = requested_blob\n\n    def to_dict(self) -> typing.Dict:\n        return {\n            self.key: self.requested_blob\n        }\n\n\nclass BlobDownloadResponse(BlobMessage):\n    key = 'incoming_blob'\n\n    def __init__(self, **response: typing.Dict) -> None:\n        incoming_blob = response[self.key]\n        self.error = None\n        self.incoming_blob = None\n        if 'error' in incoming_blob:\n            self.error = incoming_blob['error']\n        else:\n            self.incoming_blob = {'blob_hash': incoming_blob['blob_hash'], 'length': incoming_blob['length']}\n        self.length = None if not self.incoming_blob else self.incoming_blob['length']\n        self.blob_hash = None if not self.incoming_blob else self.incoming_blob['blob_hash']\n\n    def to_dict(self) -> typing.Dict:\n        return {\n            self.key if not self.error else 'error': self.incoming_blob or self.error,\n        }\n\n\nclass BlobPaymentAddressRequest(BlobMessage):\n    key = 'lbrycrd_address'\n\n    def __init__(self, lbrycrd_address: str, **kwargs) -> None:\n        self.lbrycrd_address = lbrycrd_address\n\n    def to_dict(self) -> typing.Dict:\n        return {\n            self.key: self.lbrycrd_address\n        }\n\n\nclass BlobPaymentAddressResponse(BlobPaymentAddressRequest):\n    pass\n\n\nclass BlobErrorResponse(BlobMessage):\n    key = 'error'\n\n    def __init__(self, error: str, **kwargs) -> None:\n        self.error = error\n\n    def to_dict(self) -> typing.Dict:\n        return {\n            self.key: self.error\n        }\n\n\nblob_request_types = typing.Union[BlobPriceRequest, BlobAvailabilityRequest, BlobDownloadRequest,  # pylint: disable=invalid-name\n                                  BlobPaymentAddressRequest]\nblob_response_types = typing.Union[BlobPriceResponse, BlobAvailabilityResponse, BlobDownloadResponse,  # pylint: disable=invalid-name\n                                   BlobErrorResponse, BlobPaymentAddressResponse]\n\n\ndef _parse_blob_response(response_msg: bytes) -> typing.Tuple[typing.Optional[typing.Dict], bytes]:\n    # scenarios:\n    #   <json>\n    #   <blob bytes>\n    #   <json><blob bytes>\n\n    curr_pos = 0\n    while True:\n        next_close_paren = response_msg.find(b'}', curr_pos)\n        if next_close_paren == -1:\n            return None, response_msg\n        curr_pos = next_close_paren + 1\n        try:\n            response = json.loads(response_msg[:curr_pos])\n        except ValueError:\n            continue\n        possible_response_keys = {\n            BlobPaymentAddressResponse.key,\n            BlobAvailabilityResponse.key,\n            BlobPriceResponse.key,\n            BlobDownloadResponse.key\n        }\n        if isinstance(response, dict) and response.keys():\n            if set(response.keys()).issubset(possible_response_keys):\n                return response, response_msg[curr_pos:]\n        return None, response_msg\n\n\nclass BlobRequest:\n    def __init__(self, requests: typing.List[blob_request_types]) -> None:\n        self.requests = requests\n\n    def to_dict(self):\n        d = {}\n        for request in self.requests:\n            d.update(request.to_dict())\n        return d\n\n    def _get_request(self, request_type: blob_request_types):\n        request = tuple(filter(lambda r: type(r) == request_type, self.requests))  # pylint: disable=unidiomatic-typecheck\n        if request:\n            return request[0]\n\n    def get_availability_request(self) -> typing.Optional[BlobAvailabilityRequest]:\n        response = self._get_request(BlobAvailabilityRequest)\n        if response:\n            return response\n\n    def get_price_request(self) -> typing.Optional[BlobPriceRequest]:\n        response = self._get_request(BlobPriceRequest)\n        if response:\n            return response\n\n    def get_blob_request(self) -> typing.Optional[BlobDownloadRequest]:\n        response = self._get_request(BlobDownloadRequest)\n        if response:\n            return response\n\n    def get_address_request(self) -> typing.Optional[BlobPaymentAddressRequest]:\n        response = self._get_request(BlobPaymentAddressRequest)\n        if response:\n            return response\n\n    def serialize(self) -> bytes:\n        return json.dumps(self.to_dict()).encode()\n\n    @classmethod\n    def deserialize(cls, data: bytes) -> 'BlobRequest':\n        request = json.loads(data)\n        return cls([\n            request_type(**request)\n            for request_type in (BlobPriceRequest, BlobAvailabilityRequest, BlobDownloadRequest,\n                                 BlobPaymentAddressRequest)\n            if request_type.key in request\n        ])\n\n    @classmethod\n    def make_request_for_blob_hash(cls, blob_hash: str) -> 'BlobRequest':\n        return cls(\n            [BlobAvailabilityRequest([blob_hash]), BlobPriceRequest(0.0), BlobDownloadRequest(blob_hash)]\n        )\n\n\nclass BlobResponse:\n    def __init__(self, responses: typing.List[blob_response_types], blob_data: typing.Optional[bytes] = None) -> None:\n        self.responses = responses\n        self.blob_data = blob_data\n\n    def to_dict(self):\n        d = {}\n        for response in self.responses:\n            d.update(response.to_dict())\n        return d\n\n    def _get_response(self, response_type: blob_response_types):\n        response = tuple(filter(lambda r: type(r) == response_type, self.responses))  # pylint: disable=unidiomatic-typecheck\n        if response:\n            return response[0]\n\n    def get_error_response(self) -> typing.Optional[BlobErrorResponse]:\n        error = self._get_response(BlobErrorResponse)\n        if error:\n            log.error(error)\n            return error\n\n    def get_availability_response(self) -> typing.Optional[BlobAvailabilityResponse]:\n        response = self._get_response(BlobAvailabilityResponse)\n        if response:\n            return response\n\n    def get_price_response(self) -> typing.Optional[BlobPriceResponse]:\n        response = self._get_response(BlobPriceResponse)\n        if response:\n            return response\n\n    def get_blob_response(self) -> typing.Optional[BlobDownloadResponse]:\n        response = self._get_response(BlobDownloadResponse)\n        if response:\n            return response\n\n    def get_address_response(self) -> typing.Optional[BlobPaymentAddressResponse]:\n        response = self._get_response(BlobPaymentAddressResponse)\n        if response:\n            return response\n\n    def serialize(self) -> bytes:\n        return json.dumps(self.to_dict()).encode()\n\n    @classmethod\n    def deserialize(cls, data: bytes) -> 'BlobResponse':\n        response, extra = _parse_blob_response(data)\n        requests = []\n        if response:\n            requests.extend([\n                response_type(**response)\n                for response_type in (BlobPriceResponse, BlobAvailabilityResponse, BlobDownloadResponse,\n                                      BlobErrorResponse, BlobPaymentAddressResponse)\n                if response_type.key in response\n            ])\n        return cls(requests, extra)\n"
  },
  {
    "path": "lbry/blob_exchange/server.py",
    "content": "import asyncio\nimport binascii\nimport logging\nimport socket\nimport typing\nfrom json.decoder import JSONDecodeError\nfrom lbry.blob_exchange.serialization import BlobResponse, BlobRequest, blob_response_types\nfrom lbry.blob_exchange.serialization import BlobAvailabilityResponse, BlobPriceResponse, BlobDownloadResponse, \\\n    BlobPaymentAddressResponse\n\nif typing.TYPE_CHECKING:\n    from lbry.blob.blob_manager import BlobManager\n\nlog = logging.getLogger(__name__)\n\n# a standard request will be 295 bytes\nMAX_REQUEST_SIZE = 1200\n\n\nclass BlobServerProtocol(asyncio.Protocol):\n    def __init__(self, loop: asyncio.AbstractEventLoop, blob_manager: 'BlobManager', lbrycrd_address: str,\n                 idle_timeout: float = 30.0, transfer_timeout: float = 60.0):\n        self.loop = loop\n        self.blob_manager = blob_manager\n        self.idle_timeout = idle_timeout\n        self.transfer_timeout = transfer_timeout\n        self.server_task: typing.Optional[asyncio.Task] = None\n        self.started_listening = asyncio.Event()\n        self.buf = b''\n        self.transport: typing.Optional[asyncio.Transport] = None\n        self.lbrycrd_address = lbrycrd_address\n        self.peer_address_and_port: typing.Optional[str] = None\n        self.started_transfer = asyncio.Event()\n        self.transfer_finished = asyncio.Event()\n        self.close_on_idle_task: typing.Optional[asyncio.Task] = None\n\n    async def close_on_idle(self):\n        while self.transport:\n            try:\n                await asyncio.wait_for(self.started_transfer.wait(), self.idle_timeout)\n            except asyncio.TimeoutError:\n                log.debug(\"closing idle connection from %s\", self.peer_address_and_port)\n                return self.close()\n            self.started_transfer.clear()\n            await self.transfer_finished.wait()\n            self.transfer_finished.clear()\n\n    def close(self):\n        if self.transport:\n            self.transport.close()\n\n    def connection_made(self, transport):\n        self.transport = transport\n        self.close_on_idle_task = self.loop.create_task(self.close_on_idle())\n        self.peer_address_and_port = \"%s:%i\" % self.transport.get_extra_info('peername')\n        self.blob_manager.connection_manager.connection_received(self.peer_address_and_port)\n        log.debug(\"received connection from %s\", self.peer_address_and_port)\n\n    def connection_lost(self, exc: typing.Optional[Exception]) -> None:\n        log.debug(\"lost connection from %s\", self.peer_address_and_port)\n        self.blob_manager.connection_manager.incoming_connection_lost(self.peer_address_and_port)\n        self.transport = None\n        if self.close_on_idle_task and not self.close_on_idle_task.done():\n            self.close_on_idle_task.cancel()\n        self.close_on_idle_task = None\n\n    def send_response(self, responses: typing.List[blob_response_types]):\n        to_send = []\n        while responses:\n            to_send.append(responses.pop())\n        serialized = BlobResponse(to_send).serialize()\n        self.transport.write(serialized)\n        self.blob_manager.connection_manager.sent_data(self.peer_address_and_port, len(serialized))\n\n    async def handle_request(self, request: BlobRequest):\n        addr = self.transport.get_extra_info('peername')\n        peer_address, peer_port = addr\n\n        responses = []\n        address_request = request.get_address_request()\n        if address_request:\n            responses.append(BlobPaymentAddressResponse(lbrycrd_address=self.lbrycrd_address))\n        availability_request = request.get_availability_request()\n        if availability_request:\n            responses.append(BlobAvailabilityResponse(available_blobs=list(set(\n                filter(lambda blob_hash: blob_hash in self.blob_manager.completed_blob_hashes,\n                       availability_request.requested_blobs)\n            ))))\n        price_request = request.get_price_request()\n        if price_request:\n            responses.append(BlobPriceResponse(blob_data_payment_rate='RATE_ACCEPTED'))\n        download_request = request.get_blob_request()\n\n        if download_request:\n            blob = self.blob_manager.get_blob(download_request.requested_blob)\n            if blob.get_is_verified():\n                incoming_blob = {'blob_hash': blob.blob_hash, 'length': blob.length}\n                responses.append(BlobDownloadResponse(incoming_blob=incoming_blob))\n                self.send_response(responses)\n                blob_hash = blob.blob_hash[:8]\n                log.debug(\"send %s to %s:%i\", blob_hash, peer_address, peer_port)\n                self.started_transfer.set()\n                try:\n                    sent = await asyncio.wait_for(blob.sendfile(self), self.transfer_timeout)\n                    if sent and sent > 0:\n                        self.blob_manager.connection_manager.sent_data(self.peer_address_and_port, sent)\n                        log.info(\"sent %s (%i bytes) to %s:%i\", blob_hash, sent, peer_address, peer_port)\n                    else:\n                        self.close()\n                        log.debug(\"stopped sending %s to %s:%i\", blob_hash, peer_address, peer_port)\n                        return\n                except (OSError, ValueError, asyncio.TimeoutError) as err:\n                    if isinstance(err, asyncio.TimeoutError):\n                        log.debug(\"timed out sending blob %s to %s\", blob_hash, peer_address)\n                    else:\n                        log.warning(\"could not read blob %s to send %s:%i\", blob_hash, peer_address, peer_port)\n                    self.close()\n                    return\n                finally:\n                    self.transfer_finished.set()\n            else:\n                log.info(\"don't have %s to send %s:%i\", blob.blob_hash[:8], peer_address, peer_port)\n        if responses and not self.transport.is_closing():\n            self.send_response(responses)\n\n    def data_received(self, data):\n        request = None\n        if len(self.buf) + len(data or b'') >= MAX_REQUEST_SIZE:\n            log.warning(\"request from %s is too large\", self.peer_address_and_port)\n            self.close()\n            return\n        if data:\n            self.blob_manager.connection_manager.received_data(self.peer_address_and_port, len(data))\n            _, separator, remainder = data.rpartition(b'}')\n            if not separator:\n                self.buf += data\n                return\n            try:\n                request = BlobRequest.deserialize(self.buf + data)\n                self.buf = remainder\n            except (UnicodeDecodeError, JSONDecodeError):\n                log.error(\"request from %s is not valid json (%i bytes): %s\", self.peer_address_and_port,\n                          len(self.buf + data), '' if not data else binascii.hexlify(self.buf + data).decode())\n                self.close()\n                return\n        if not request.requests:\n            log.error(\"failed to decode request from %s (%i bytes): %s\", self.peer_address_and_port,\n                      len(self.buf + data), '' if not data else binascii.hexlify(self.buf + data).decode())\n            self.close()\n            return\n        self.loop.create_task(self.handle_request(request))\n\n\nclass BlobServer:\n    def __init__(self, loop: asyncio.AbstractEventLoop, blob_manager: 'BlobManager', lbrycrd_address: str,\n                 idle_timeout: float = 30.0, transfer_timeout: float = 60.0):\n        self.loop = loop\n        self.blob_manager = blob_manager\n        self.server_task: typing.Optional[asyncio.Task] = None\n        self.started_listening = asyncio.Event()\n        self.lbrycrd_address = lbrycrd_address\n        self.idle_timeout = idle_timeout\n        self.transfer_timeout = transfer_timeout\n        self.server_protocol_class = BlobServerProtocol\n\n    def start_server(self, port: int, interface: typing.Optional[str] = '0.0.0.0'):\n        if self.server_task is not None:\n            raise Exception(\"already running\")\n\n        async def _start_server():\n            # checking if the port is in use\n            # thx https://stackoverflow.com/a/52872579\n            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:\n                if s.connect_ex(('localhost', port)) == 0:\n                    # the port is already in use!\n                    log.error(\"Failed to bind TCP %s:%d\", interface, port)\n\n            server = await self.loop.create_server(\n                lambda: self.server_protocol_class(self.loop, self.blob_manager, self.lbrycrd_address,\n                                                   self.idle_timeout, self.transfer_timeout),\n                interface, port\n            )\n            self.started_listening.set()\n            log.info(\"Blob server listening on TCP %s:%i\", interface, port)\n            async with server:\n                await server.serve_forever()\n\n        self.server_task = self.loop.create_task(_start_server())\n\n    def stop_server(self):\n        if self.server_task:\n            self.server_task.cancel()\n            self.server_task = None\n            log.info(\"Stopped blob server\")\n"
  },
  {
    "path": "lbry/build_info.py",
    "content": "# don't touch this. CI server changes this during build/deployment\nBUILD = \"dev\"\nCOMMIT_HASH = \"none\"\nDOCKER_TAG = \"none\"\n"
  },
  {
    "path": "lbry/conf.py",
    "content": "import os\nimport re\nimport sys\nimport logging\nfrom typing import List, Dict, Tuple, Union, TypeVar, Generic, Optional\nfrom argparse import ArgumentParser\nfrom contextlib import contextmanager\nfrom appdirs import user_data_dir, user_config_dir\nimport yaml\nfrom lbry.error import InvalidCurrencyError\nfrom lbry.dht import constants\nfrom lbry.wallet.coinselection import STRATEGIES\n\nlog = logging.getLogger(__name__)\n\n\nNOT_SET = type('NOT_SET', (object,), {})  # pylint: disable=invalid-name\nT = TypeVar('T')\n\nCURRENCIES = {\n    'BTC': {'type': 'crypto'},\n    'LBC': {'type': 'crypto'},\n    'USD': {'type': 'fiat'},\n}\n\n\nclass Setting(Generic[T]):\n\n    def __init__(self, doc: str, default: Optional[T] = None,\n                 previous_names: Optional[List[str]] = None,\n                 metavar: Optional[str] = None):\n        self.doc = doc\n        self.default = default\n        self.previous_names = previous_names or []\n        self.metavar = metavar\n\n    def __set_name__(self, owner, name):\n        self.name = name  # pylint: disable=attribute-defined-outside-init\n\n    @property\n    def cli_name(self):\n        return f\"--{self.name.replace('_', '-')}\"\n\n    @property\n    def no_cli_name(self):\n        return f\"--no-{self.name.replace('_', '-')}\"\n\n    def __get__(self, obj: Optional['BaseConfig'], owner) -> T:\n        if obj is None:\n            return self\n        for location in obj.search_order:\n            if self.name in location:\n                return location[self.name]\n        return self.default\n\n    def __set__(self, obj: 'BaseConfig', val: Union[T, NOT_SET]):\n        if val == NOT_SET:\n            for location in obj.modify_order:\n                if self.name in location:\n                    del location[self.name]\n        else:\n            self.validate(val)\n            for location in obj.modify_order:\n                location[self.name] = val\n\n    def is_set(self, obj: 'BaseConfig') -> bool:\n        for location in obj.search_order:\n            if self.name in location:\n                return True\n        return False\n\n    def is_set_to_default(self, obj: 'BaseConfig') -> bool:\n        for location in obj.search_order:\n            if self.name in location:\n                return location[self.name] == self.default\n        return False\n\n    def validate(self, value):\n        raise NotImplementedError()\n\n    def deserialize(self, value):  # pylint: disable=no-self-use\n        return value\n\n    def serialize(self, value):  # pylint: disable=no-self-use\n        return value\n\n    def contribute_to_argparse(self, parser: ArgumentParser):\n        parser.add_argument(\n            self.cli_name,\n            help=self.doc,\n            metavar=self.metavar,\n            default=NOT_SET\n        )\n\n\nclass String(Setting[str]):\n    def validate(self, value):\n        assert isinstance(value, str), \\\n            f\"Setting '{self.name}' must be a string.\"\n\n    # TODO: removes this after pylint starts to understand generics\n    def __get__(self, obj: Optional['BaseConfig'], owner) -> str:  # pylint: disable=useless-super-delegation\n        return super().__get__(obj, owner)\n\n\nclass Integer(Setting[int]):\n    def validate(self, value):\n        assert isinstance(value, int), \\\n            f\"Setting '{self.name}' must be an integer.\"\n\n    def deserialize(self, value):\n        return int(value)\n\n\nclass Float(Setting[float]):\n    def validate(self, value):\n        assert isinstance(value, float), \\\n            f\"Setting '{self.name}' must be a decimal.\"\n\n    def deserialize(self, value):\n        return float(value)\n\n\nclass Toggle(Setting[bool]):\n    def validate(self, value):\n        assert isinstance(value, bool), \\\n            f\"Setting '{self.name}' must be a true/false value.\"\n\n    def contribute_to_argparse(self, parser: ArgumentParser):\n        parser.add_argument(\n            self.cli_name,\n            help=self.doc,\n            action=\"store_true\",\n            default=NOT_SET\n        )\n        parser.add_argument(\n            self.no_cli_name,\n            help=f\"Opposite of {self.cli_name}\",\n            dest=self.name,\n            action=\"store_false\",\n            default=NOT_SET\n        )\n\n\nclass Path(String):\n    def __init__(self, doc: str, *args, default: str = '', **kwargs):\n        super().__init__(doc, default, *args, **kwargs)\n\n    def __get__(self, obj, owner) -> str:\n        value = super().__get__(obj, owner)\n        if isinstance(value, str):\n            return os.path.expanduser(os.path.expandvars(value))\n        return value\n\n\nclass MaxKeyFee(Setting[dict]):\n\n    def validate(self, value):\n        if value is not None:\n            assert isinstance(value, dict) and set(value) == {'currency', 'amount'}, \\\n                f\"Setting '{self.name}' must be a dict like \\\"{{'amount': 50.0, 'currency': 'USD'}}\\\".\"\n            if value[\"currency\"] not in CURRENCIES:\n                raise InvalidCurrencyError(value[\"currency\"])\n\n    @staticmethod\n    def _parse_list(l):\n        if l == ['null']:\n            return None\n        assert len(l) == 2, (\n            'Max key fee is made up of either two values: '\n            '\"AMOUNT CURRENCY\", or \"null\" (to set no limit)'\n        )\n        try:\n            amount = float(l[0])\n        except ValueError:\n            raise AssertionError('First value in max key fee is a decimal: \"AMOUNT CURRENCY\"')\n        currency = str(l[1]).upper()\n        if currency not in CURRENCIES:\n            raise InvalidCurrencyError(currency)\n        return {'amount': amount, 'currency': currency}\n\n    def deserialize(self, value):\n        if value is None:\n            return\n        if isinstance(value, dict):\n            return {\n                'currency': value['currency'],\n                'amount': float(value['amount']),\n            }\n        if isinstance(value, str):\n            value = value.split()\n        if isinstance(value, list):\n            return self._parse_list(value)\n        raise AssertionError('Invalid max key fee.')\n\n    def contribute_to_argparse(self, parser: ArgumentParser):\n        parser.add_argument(\n            self.cli_name,\n            help=self.doc,\n            nargs='+',\n            metavar=('AMOUNT', 'CURRENCY'),\n            default=NOT_SET\n        )\n        parser.add_argument(\n            self.no_cli_name,\n            help=\"Disable maximum key fee check.\",\n            dest=self.name,\n            const=None,\n            action=\"store_const\",\n            default=NOT_SET\n        )\n\n\nclass StringChoice(String):\n    def __init__(self, doc: str, valid_values: List[str], default: str, *args, **kwargs):\n        super().__init__(doc, default, *args, **kwargs)\n        if not valid_values:\n            raise ValueError(\"No valid values provided\")\n        if default not in valid_values:\n            raise ValueError(f\"Default value must be one of: {', '.join(valid_values)}\")\n        self.valid_values = valid_values\n\n    def validate(self, value):\n        super().validate(value)\n        if value not in self.valid_values:\n            raise ValueError(f\"Setting '{self.name}' value must be one of: {', '.join(self.valid_values)}\")\n\n\nclass ListSetting(Setting[list]):\n\n    def validate(self, value):\n        assert isinstance(value, (tuple, list)), \\\n            f\"Setting '{self.name}' must be a tuple or list.\"\n\n    def contribute_to_argparse(self, parser: ArgumentParser):\n        parser.add_argument(\n            self.cli_name,\n            help=self.doc,\n            action='append'\n        )\n\n\nclass Servers(ListSetting):\n\n    def validate(self, value):\n        assert isinstance(value, (tuple, list)), \\\n            f\"Setting '{self.name}' must be a tuple or list of servers.\"\n        for idx, server in enumerate(value):\n            assert isinstance(server, (tuple, list)) and len(server) == 2, \\\n                f\"Server defined '{server}' at index {idx} in setting \" \\\n                f\"'{self.name}' must be a tuple or list of two items.\"\n            assert isinstance(server[0], str), \\\n                f\"Server defined '{server}' at index {idx} in setting \" \\\n                f\"'{self.name}' must be have hostname as string in first position.\"\n            assert isinstance(server[1], int), \\\n                f\"Server defined '{server}' at index {idx} in setting \" \\\n                f\"'{self.name}' must be have port as int in second position.\"\n\n    def deserialize(self, value):\n        servers = []\n        if isinstance(value, list):\n            for server in value:\n                if isinstance(server, str) and server.count(':') == 1:\n                    host, port = server.split(':')\n                    try:\n                        servers.append((host, int(port)))\n                    except ValueError:\n                        pass\n        return servers\n\n    def serialize(self, value):\n        if value:\n            return [f\"{host}:{port}\" for host, port in value]\n        return value\n\n\nclass Strings(ListSetting):\n\n    def validate(self, value):\n        assert isinstance(value, (tuple, list)), \\\n            f\"Setting '{self.name}' must be a tuple or list of strings.\"\n        for idx, string in enumerate(value):\n            assert isinstance(string, str), \\\n                f\"Value of '{string}' at index {idx} in setting \" \\\n                f\"'{self.name}' must be a string.\"\n\n\nclass KnownHubsList:\n\n    def __init__(self, config: 'Config' = None, file_name: str = 'known_hubs.yml'):\n        self.file_name = file_name\n        self.path = os.path.join(config.wallet_dir, self.file_name) if config else None\n        self.hubs: Dict[Tuple[str, int], Dict] = {}\n        if self.exists:\n            self.load()\n\n    @property\n    def exists(self):\n        return self.path and os.path.exists(self.path)\n\n    @property\n    def serialized(self) -> Dict[str, Dict]:\n        return {f\"{host}:{port}\": details for (host, port), details in self.hubs.items()}\n\n    def filter(self, match_none=False, **kwargs):\n        if not kwargs:\n            return self.hubs\n        result = {}\n        for hub, details in self.hubs.items():\n            for key, constraint in kwargs.items():\n                value = details.get(key)\n                if value == constraint or (match_none and value is None):\n                    result[hub] = details\n                    break\n        return result\n\n    def load(self):\n        if self.path:\n            with open(self.path, 'r') as known_hubs_file:\n                raw = known_hubs_file.read()\n                for hub, details in yaml.safe_load(raw).items():\n                    self.set(hub, details)\n\n    def save(self):\n        if self.path:\n            with open(self.path, 'w') as known_hubs_file:\n                known_hubs_file.write(yaml.safe_dump(self.serialized, default_flow_style=False))\n\n    def set(self, hub: str, details: Dict):\n        if hub and hub.count(':') == 1:\n            host, port = hub.split(':')\n            hub_parts = (host, int(port))\n            if hub_parts not in self.hubs:\n                self.hubs[hub_parts] = details\n                return hub\n\n    def add_hubs(self, hubs: List[str]):\n        added = False\n        for hub in hubs:\n            if self.set(hub, {}) is not None:\n                added = True\n        return added\n\n    def items(self):\n        return self.hubs.items()\n\n    def __bool__(self):\n        return len(self) > 0\n\n    def __len__(self):\n        return self.hubs.__len__()\n\n    def __iter__(self):\n        return iter(self.hubs)\n\n\nclass EnvironmentAccess:\n    PREFIX = 'LBRY_'\n\n    def __init__(self, config: 'BaseConfig', environ: dict):\n        self.configuration = config\n        self.data = {}\n        if environ:\n            self.load(environ)\n\n    def load(self, environ):\n        for setting in self.configuration.get_settings():\n            value = environ.get(f'{self.PREFIX}{setting.name.upper()}', NOT_SET)\n            if value != NOT_SET and not (isinstance(setting, ListSetting) and value is None):\n                self.data[setting.name] = setting.deserialize(value)\n\n    def __contains__(self, item: str):\n        return item in self.data\n\n    def __getitem__(self, item: str):\n        return self.data[item]\n\n\nclass ArgumentAccess:\n\n    def __init__(self, config: 'BaseConfig', args: dict):\n        self.configuration = config\n        self.args = {}\n        if args:\n            self.load(args)\n\n    def load(self, args):\n        for setting in self.configuration.get_settings():\n            value = getattr(args, setting.name, NOT_SET)\n            if value != NOT_SET and not (isinstance(setting, ListSetting) and value is None):\n                self.args[setting.name] = setting.deserialize(value)\n\n    def __contains__(self, item: str):\n        return item in self.args\n\n    def __getitem__(self, item: str):\n        return self.args[item]\n\n\nclass ConfigFileAccess:\n\n    def __init__(self, config: 'BaseConfig', path: str):\n        self.configuration = config\n        self.path = path\n        self.data = {}\n        if self.exists:\n            self.load()\n\n    @property\n    def exists(self):\n        return self.path and os.path.exists(self.path)\n\n    def load(self):\n        cls = type(self.configuration)\n        with open(self.path, 'r') as config_file:\n            raw = config_file.read()\n        serialized = yaml.safe_load(raw) or {}\n        for key, value in serialized.items():\n            attr = getattr(cls, key, None)\n            if attr is None:\n                for setting in self.configuration.settings:\n                    if key in setting.previous_names:\n                        attr = setting\n                        break\n            if attr is not None:\n                self.data[key] = attr.deserialize(value)\n\n    def save(self):\n        cls = type(self.configuration)\n        serialized = {}\n        for key, value in self.data.items():\n            attr = getattr(cls, key)\n            serialized[key] = attr.serialize(value)\n        with open(self.path, 'w') as config_file:\n            config_file.write(yaml.safe_dump(serialized, default_flow_style=False))\n\n    def upgrade(self) -> bool:\n        upgraded = False\n        for key in list(self.data):\n            for setting in self.configuration.settings:\n                if key in setting.previous_names:\n                    self.data[setting.name] = self.data[key]\n                    del self.data[key]\n                    upgraded = True\n                    break\n        return upgraded\n\n    def __contains__(self, item: str):\n        return item in self.data\n\n    def __getitem__(self, item: str):\n        return self.data[item]\n\n    def __setitem__(self, key, value):\n        self.data[key] = value\n\n    def __delitem__(self, key):\n        del self.data[key]\n\n\nTBC = TypeVar('TBC', bound='BaseConfig')\n\n\nclass BaseConfig:\n\n    config = Path(\"Path to configuration file.\", metavar='FILE')\n\n    def __init__(self, **kwargs):\n        self.runtime = {}      # set internally or by various API calls\n        self.arguments = {}    # from command line arguments\n        self.environment = {}  # from environment variables\n        self.persisted = {}    # from config file\n        self._updating_config = False\n        for key, value in kwargs.items():\n            setattr(self, key, value)\n\n    @contextmanager\n    def update_config(self):\n        self._updating_config = True\n        yield self\n        self._updating_config = False\n        if isinstance(self.persisted, ConfigFileAccess):\n            self.persisted.save()\n\n    @property\n    def modify_order(self):\n        locations = [self.runtime]\n        if self._updating_config:\n            locations.append(self.persisted)\n        return locations\n\n    @property\n    def search_order(self):\n        return [\n            self.runtime,\n            self.arguments,\n            self.environment,\n            self.persisted\n        ]\n\n    @classmethod\n    def get_settings(cls):\n        for attr in dir(cls):\n            setting = getattr(cls, attr)\n            if isinstance(setting, Setting):\n                yield setting\n\n    @property\n    def settings(self):\n        return self.get_settings()\n\n    @property\n    def settings_dict(self):\n        return {\n            setting.name: getattr(self, setting.name) for setting in self.settings\n        }\n\n    @classmethod\n    def create_from_arguments(cls, args) -> TBC:\n        conf = cls()\n        conf.set_arguments(args)\n        conf.set_environment()\n        conf.set_persisted()\n        return conf\n\n    @classmethod\n    def contribute_to_argparse(cls, parser: ArgumentParser):\n        for setting in cls.get_settings():\n            setting.contribute_to_argparse(parser)\n\n    def set_arguments(self, args):\n        self.arguments = ArgumentAccess(self, args)\n\n    def set_environment(self, environ=None):\n        self.environment = EnvironmentAccess(self, environ or os.environ)\n\n    def set_persisted(self, config_file_path=None):\n        if config_file_path is None:\n            config_file_path = self.config\n\n        if not config_file_path:\n            return\n\n        ext = os.path.splitext(config_file_path)[1]\n        assert ext in ('.yml', '.yaml'),\\\n            f\"File extension '{ext}' is not supported, \" \\\n            f\"configuration file must be in YAML (.yaml).\"\n\n        self.persisted = ConfigFileAccess(self, config_file_path)\n        if self.persisted.upgrade():\n            self.persisted.save()\n\n\nclass TranscodeConfig(BaseConfig):\n\n    ffmpeg_path = String('A list of places to check for ffmpeg and ffprobe. '\n                         f'$data_dir/ffmpeg/bin and $PATH are checked afterward. Separator: {os.pathsep}',\n                         '', previous_names=['ffmpeg_folder'])\n    video_encoder = String('FFmpeg codec and parameters for the video encoding. '\n                           'Example: libaom-av1 -crf 25 -b:v 0 -strict experimental',\n                           'libx264 -crf 24 -preset faster -pix_fmt yuv420p')\n    video_bitrate_maximum = Integer('Maximum bits per second allowed for video streams (0 to disable).', 5_000_000)\n    video_scaler = String('FFmpeg scaling parameters for reducing bitrate. '\n                          'Example: -vf \"scale=-2:720,fps=24\" -maxrate 5M -bufsize 3M',\n                          r'-vf \"scale=if(gte(iw\\,ih)\\,min(1920\\,iw)\\,-2):if(lt(iw\\,ih)\\,min(1920\\,ih)\\,-2)\" '\n                          r'-maxrate 5500K -bufsize 5000K')\n    audio_encoder = String('FFmpeg codec and parameters for the audio encoding. '\n                           'Example: libopus -b:a 128k',\n                           'aac -b:a 160k')\n    volume_filter = String('FFmpeg filter for audio normalization. Exmple: -af loudnorm', '')\n    volume_analysis_time = Integer('Maximum seconds into the file that we examine audio volume (0 to disable).', 240)\n\n\nclass CLIConfig(TranscodeConfig):\n\n    api = String('Host name and port for lbrynet daemon API.', 'localhost:5279', metavar='HOST:PORT')\n\n    @property\n    def api_connection_url(self) -> str:\n        return f\"http://{self.api}/lbryapi\"\n\n    @property\n    def api_host(self):\n        return self.api.split(':')[0]\n\n    @property\n    def api_port(self):\n        return int(self.api.split(':')[1])\n\n\nclass Config(CLIConfig):\n\n    jurisdiction = String(\"Limit interactions to wallet server in this jurisdiction.\")\n\n    # directories\n    data_dir = Path(\"Directory path to store blobs.\", metavar='DIR')\n    download_dir = Path(\n        \"Directory path to place assembled files downloaded from LBRY.\",\n        previous_names=['download_directory'], metavar='DIR'\n    )\n    wallet_dir = Path(\n        \"Directory containing a 'wallets' subdirectory with 'default_wallet' file.\",\n        previous_names=['lbryum_wallet_dir'], metavar='DIR'\n    )\n    wallets = Strings(\n        \"Wallet files in 'wallet_dir' to load at startup.\",\n        ['default_wallet']\n    )\n\n    # network\n    use_upnp = Toggle(\n        \"Use UPnP to setup temporary port redirects for the DHT and the hosting of blobs. If you manually forward\"\n        \"ports or have firewall rules you likely want to disable this.\", True\n    )\n    udp_port = Integer(\"UDP port for communicating on the LBRY DHT\", 4444, previous_names=['dht_node_port'])\n    tcp_port = Integer(\"TCP port to listen for incoming blob requests\", 4444, previous_names=['peer_port'])\n    prometheus_port = Integer(\"Port to expose prometheus metrics (off by default)\", 0)\n    network_interface = String(\"Interface to use for the DHT and blob exchange\", '0.0.0.0')\n\n    # routing table\n    split_buckets_under_index = Integer(\n        \"Routing table bucket index below which we always split the bucket if given a new key to add to it and \"\n        \"the bucket is full. As this value is raised the depth of the routing table (and number of peers in it) \"\n        \"will increase. This setting is used by seed nodes, you probably don't want to change it during normal \"\n        \"use.\", 2\n    )\n    is_bootstrap_node = Toggle(\n        \"When running as a bootstrap node, disable all logic related to balancing the routing table, so we can \"\n        \"add as many peers as possible and better help first-runs.\", False\n    )\n\n    # protocol timeouts\n    download_timeout = Float(\"Cumulative timeout for a stream to begin downloading before giving up\", 30.0)\n    blob_download_timeout = Float(\"Timeout to download a blob from a peer\", 30.0)\n    hub_timeout = Float(\"Timeout when making a hub request\", 30.0)\n    peer_connect_timeout = Float(\"Timeout to establish a TCP connection to a peer\", 3.0)\n    node_rpc_timeout = Float(\"Timeout when making a DHT request\", constants.RPC_TIMEOUT)\n\n    # blob announcement and download\n    save_blobs = Toggle(\"Save encrypted blob files for hosting, otherwise download blobs to memory only.\", True)\n    network_storage_limit = Integer(\"Disk space in MB to be allocated for helping the P2P network. 0 = disable\", 0)\n    blob_storage_limit = Integer(\"Disk space in MB to be allocated for blob storage. 0 = no limit\", 0)\n    blob_lru_cache_size = Integer(\n        \"LRU cache size for decrypted downloaded blobs used to minimize re-downloading the same blobs when \"\n        \"replying to a range request. Set to 0 to disable.\", 32\n    )\n    announce_head_and_sd_only = Toggle(\n        \"Announce only the descriptor and first (rather than all) data blob for a stream to the DHT\", True,\n        previous_names=['announce_head_blobs_only']\n    )\n    concurrent_blob_announcers = Integer(\n        \"Number of blobs to iteratively announce at once, set to 0 to disable\", 10,\n        previous_names=['concurrent_announcers']\n    )\n    max_connections_per_download = Integer(\n        \"Maximum number of peers to connect to while downloading a blob\", 4,\n        previous_names=['max_connections_per_stream']\n    )\n    concurrent_hub_requests = Integer(\"Maximum number of concurrent hub requests\", 32)\n    fixed_peer_delay = Float(\n        \"Amount of seconds before adding the reflector servers as potential peers to download from in case dht\"\n        \"peers are not found or are slow\", 2.0\n    )\n    max_key_fee = MaxKeyFee(\n        \"Don't download streams with fees exceeding this amount. When set to \"\n        \"null, the amount is unbounded.\", {'currency': 'USD', 'amount': 50.0}\n    )\n    max_wallet_server_fee = String(\"Maximum daily LBC amount allowed as payment for wallet servers.\", \"0.0\")\n\n    # reflector settings\n    reflect_streams = Toggle(\n        \"Upload completed streams (published and downloaded) reflector in order to re-host them\", True,\n        previous_names=['reflect_uploads']\n    )\n    concurrent_reflector_uploads = Integer(\n        \"Maximum number of streams to upload to a reflector server at a time\", 10\n    )\n\n    # servers\n    reflector_servers = Servers(\"Reflector re-hosting servers for mirroring publishes\", [\n        ('reflector.lbry.com', 5566)\n    ])\n\n    fixed_peers = Servers(\"Fixed peers to fall back to if none are found on P2P for a blob\", [\n        ('cdn.reflector.lbry.com', 5567)\n    ])\n\n    tracker_servers = Servers(\"BitTorrent-compatible (BEP15) UDP trackers for helping P2P discovery\", [\n        ('tracker.lbry.com', 9252),\n        ('tracker.lbry.grin.io', 9252),\n        ('tracker.lbry.pigg.es', 9252),\n        ('tracker.lizard.technology', 9252),\n        ('s1.lbry.network', 9252),\n    ])\n\n    lbryum_servers = Servers(\"SPV wallet servers\", [\n        ('spv11.lbry.com', 50001),\n        ('spv12.lbry.com', 50001),\n        ('spv13.lbry.com', 50001),\n        ('spv14.lbry.com', 50001),\n        ('spv15.lbry.com', 50001),\n        ('spv16.lbry.com', 50001),\n        ('spv17.lbry.com', 50001),\n        ('spv18.lbry.com', 50001),\n        ('spv19.lbry.com', 50001),\n        ('hub.lbry.grin.io', 50001),\n        ('hub.lizard.technology', 50001),\n        ('s1.lbry.network', 50001),\n    ])\n    known_dht_nodes = Servers(\"Known nodes for bootstrapping connection to the DHT\", [\n        ('dht.lbry.grin.io', 4444),  # Grin\n        ('dht.lbry.madiator.com', 4444),  # Madiator\n        ('dht.lbry.pigg.es', 4444), # Pigges\n        ('lbrynet1.lbry.com', 4444),  # US EAST\n        ('lbrynet2.lbry.com', 4444),  # US WEST\n        ('lbrynet3.lbry.com', 4444),  # EU\n        ('lbrynet4.lbry.com', 4444),  # ASIA\n        ('dht.lizard.technology', 4444),  # Jack\n        ('s2.lbry.network', 4444),\n    ])\n\n    # blockchain\n    blockchain_name = String(\"Blockchain name - lbrycrd_main, lbrycrd_regtest, or lbrycrd_testnet\", 'lbrycrd_main')\n\n    # daemon\n    save_files = Toggle(\"Save downloaded files when calling `get` by default\", False)\n    components_to_skip = Strings(\"components which will be skipped during start-up of daemon\", [])\n    share_usage_data = Toggle(\n        \"Whether to share usage stats and diagnostic info with LBRY.\", False,\n        previous_names=['upload_log', 'upload_log', 'share_debug_info']\n    )\n    track_bandwidth = Toggle(\"Track bandwidth usage\", True)\n    allowed_origin = String(\n        \"Allowed `Origin` header value for API request (sent by browser), use * to allow \"\n        \"all hosts; default is to only allow API requests with no `Origin` value.\", \"\")\n\n    # media server\n    streaming_server = String('Host name and port to serve streaming media over range requests',\n                              'localhost:5280', metavar='HOST:PORT')\n    streaming_get = Toggle(\"Enable the /get endpoint for the streaming media server. \"\n                           \"Disable to prevent new streams from being added.\", True)\n\n    coin_selection_strategy = StringChoice(\n        \"Strategy to use when selecting UTXOs for a transaction\",\n        STRATEGIES, \"prefer_confirmed\"\n    )\n\n    transaction_cache_size = Integer(\"Transaction cache size\", 2 ** 17)\n    save_resolved_claims = Toggle(\n        \"Save content claims to the database when they are resolved to keep file_list up to date, \"\n        \"only disable this if file_x commands are not needed\", True\n    )\n\n    @property\n    def streaming_host(self):\n        return self.streaming_server.split(':')[0]\n\n    @property\n    def streaming_port(self):\n        return int(self.streaming_server.split(':')[1])\n\n    def __init__(self, **kwargs):\n        super().__init__(**kwargs)\n        self.set_default_paths()\n        self.known_hubs = KnownHubsList(self)\n\n    def set_default_paths(self):\n        if 'darwin' in sys.platform.lower():\n            get_directories = get_darwin_directories\n        elif 'win' in sys.platform.lower():\n            get_directories = get_windows_directories\n        elif 'linux' in sys.platform.lower():\n            get_directories = get_linux_directories\n        else:\n            return\n        cls = type(self)\n        cls.data_dir.default, cls.wallet_dir.default, cls.download_dir.default = get_directories()\n        cls.config.default = os.path.join(\n            self.data_dir, 'daemon_settings.yml'\n        )\n\n    @property\n    def log_file_path(self):\n        return os.path.join(self.data_dir, 'lbrynet.log')\n\n\ndef get_windows_directories() -> Tuple[str, str, str]:\n    from lbry.winpaths import get_path, FOLDERID, UserHandle, \\\n        PathNotFoundException  # pylint: disable=import-outside-toplevel\n\n    try:\n        download_dir = get_path(FOLDERID.Downloads, UserHandle.current)\n    except PathNotFoundException:\n        download_dir = os.getcwd()\n\n    # old\n    appdata = get_path(FOLDERID.RoamingAppData, UserHandle.current)\n    data_dir = os.path.join(appdata, 'lbrynet')\n    lbryum_dir = os.path.join(appdata, 'lbryum')\n    if os.path.isdir(data_dir) or os.path.isdir(lbryum_dir):\n        return data_dir, lbryum_dir, download_dir\n\n    # new\n    data_dir = user_data_dir('lbrynet', 'lbry')\n    lbryum_dir = user_data_dir('lbryum', 'lbry')\n    return data_dir, lbryum_dir, download_dir\n\n\ndef get_darwin_directories() -> Tuple[str, str, str]:\n    data_dir = user_data_dir('LBRY')\n    lbryum_dir = os.path.expanduser('~/.lbryum')\n    download_dir = os.path.expanduser('~/Downloads')\n    return data_dir, lbryum_dir, download_dir\n\n\ndef get_linux_directories() -> Tuple[str, str, str]:\n    try:\n        with open(os.path.join(user_config_dir(), 'user-dirs.dirs'), 'r') as xdg:\n            down_dir = re.search(r'XDG_DOWNLOAD_DIR=(.+)', xdg.read())\n        if down_dir:\n            down_dir = re.sub(r'\\$HOME', os.getenv('HOME') or os.path.expanduser(\"~/\"), down_dir.group(1))\n            download_dir = re.sub('\\\"', '', down_dir)\n    except OSError:\n        download_dir = os.getenv('XDG_DOWNLOAD_DIR')\n    if not download_dir:\n        download_dir = os.path.expanduser('~/Downloads')\n\n    # old\n    data_dir = os.path.expanduser('~/.lbrynet')\n    lbryum_dir = os.path.expanduser('~/.lbryum')\n    if os.path.isdir(data_dir) or os.path.isdir(lbryum_dir):\n        return data_dir, lbryum_dir, download_dir\n\n    # new\n    return user_data_dir('lbry/lbrynet'), user_data_dir('lbry/lbryum'), download_dir\n"
  },
  {
    "path": "lbry/connection_manager.py",
    "content": "import time\nimport asyncio\nimport typing\nimport collections\nimport logging\n\nlog = logging.getLogger(__name__)\n\n\nCONNECTED_EVENT = \"connected\"\nDISCONNECTED_EVENT = \"disconnected\"\nTRANSFERRED_EVENT = \"transferred\"\n\n\nclass ConnectionManager:\n    def __init__(self, loop: asyncio.AbstractEventLoop):\n        self.loop = loop\n        self.incoming_connected: typing.Set[str] = set()\n        self.incoming: typing.DefaultDict[str, int] = collections.defaultdict(int)\n        self.outgoing_connected: typing.Set[str] = set()\n        self.outgoing: typing.DefaultDict[str, int] = collections.defaultdict(int)\n        self._max_incoming_mbs = 0.0\n        self._max_outgoing_mbs = 0.0\n        self._status = {}\n        self._running = False\n        self._task: typing.Optional[asyncio.Task] = None\n\n    @property\n    def status(self):\n        return self._status\n\n    def sent_data(self, host_and_port: str, size: int):\n        if self._running:\n            self.outgoing[host_and_port] += size\n\n    def received_data(self, host_and_port: str, size: int):\n        if self._running:\n            self.incoming[host_and_port] += size\n\n    def connection_made(self, host_and_port: str):\n        if self._running:\n            self.outgoing_connected.add(host_and_port)\n\n    def connection_received(self, host_and_port: str):\n        # self.incoming_connected.add(host_and_port)\n        pass\n\n    def outgoing_connection_lost(self, host_and_port: str):\n        if self._running and host_and_port in self.outgoing_connected:\n            self.outgoing_connected.remove(host_and_port)\n\n    def incoming_connection_lost(self, host_and_port: str):\n        if self._running and host_and_port in self.incoming_connected:\n            self.incoming_connected.remove(host_and_port)\n\n    async def _update(self):\n        self._status = {\n            'incoming_bps': {},\n            'outgoing_bps': {},\n            'total_incoming_mbs': 0.0,\n            'total_outgoing_mbs': 0.0,\n            'total_sent': 0,\n            'total_received': 0,\n            'max_incoming_mbs': 0.0,\n            'max_outgoing_mbs': 0.0\n        }\n\n        while True:\n            last = time.perf_counter()\n            await asyncio.sleep(0.1)\n            self._status['incoming_bps'].clear()\n            self._status['outgoing_bps'].clear()\n            now = time.perf_counter()\n            while self.outgoing:\n                k, sent = self.outgoing.popitem()\n                self._status['total_sent'] += sent\n                self._status['outgoing_bps'][k] = sent / (now - last)\n            while self.incoming:\n                k, received = self.incoming.popitem()\n                self._status['total_received'] += received\n                self._status['incoming_bps'][k] = received / (now - last)\n            self._status['total_outgoing_mbs'] = int(sum(list(self._status['outgoing_bps'].values())\n                                                         )) / 1000000.0\n            self._status['total_incoming_mbs'] = int(sum(list(self._status['incoming_bps'].values())\n                                                         )) / 1000000.0\n            self._max_incoming_mbs = max(self._max_incoming_mbs, self._status['total_incoming_mbs'])\n            self._max_outgoing_mbs = max(self._max_outgoing_mbs, self._status['total_outgoing_mbs'])\n            self._status['max_incoming_mbs'] = self._max_incoming_mbs\n            self._status['max_outgoing_mbs'] = self._max_outgoing_mbs\n\n    def stop(self):\n        if self._task:\n            self._task.cancel()\n            self._task = None\n        self.outgoing.clear()\n        self.outgoing_connected.clear()\n        self.incoming.clear()\n        self.incoming_connected.clear()\n        self._status.clear()\n        self._running = False\n\n    def start(self):\n        self.stop()\n        self._running = True\n        self._task = self.loop.create_task(self._update())\n"
  },
  {
    "path": "lbry/constants.py",
    "content": "CENT = 1000000\nCOIN = 100*CENT\n"
  },
  {
    "path": "lbry/crypto/__init__.py",
    "content": ""
  },
  {
    "path": "lbry/crypto/base58.py",
    "content": "from lbry.crypto.hash import double_sha256\nfrom lbry.crypto.util import bytes_to_int, int_to_bytes\n\n\nclass Base58Error(Exception):\n    \"\"\" Exception used for Base58 errors. \"\"\"\n\n\nclass Base58:\n    \"\"\" Class providing base 58 functionality. \"\"\"\n\n    chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'\n    assert len(chars) == 58\n    char_map = {c: n for n, c in enumerate(chars)}\n\n    @classmethod\n    def char_value(cls, c):\n        val = cls.char_map.get(c)\n        if val is None:\n            raise Base58Error(f'invalid base 58 character \"{c}\"')\n        return val\n\n    @classmethod\n    def decode(cls, txt):\n        \"\"\" Decodes txt into a big-endian bytearray. \"\"\"\n        if isinstance(txt, memoryview):\n            txt = str(txt)\n\n        if isinstance(txt, bytes):\n            txt = txt.decode()\n\n        if not isinstance(txt, str):\n            raise TypeError('a string is required')\n\n        if not txt:\n            raise Base58Error('string cannot be empty')\n\n        value = 0\n        for c in txt:\n            value = value * 58 + cls.char_value(c)\n\n        result = int_to_bytes(value)\n\n        # Prepend leading zero bytes if necessary\n        count = 0\n        for c in txt:\n            if c != '1':\n                break\n            count += 1\n        if count:\n            result = bytes((0,)) * count + result\n\n        return result\n\n    @classmethod\n    def encode(cls, be_bytes):\n        \"\"\"Converts a big-endian bytearray into a base58 string.\"\"\"\n        value = bytes_to_int(be_bytes)\n\n        txt = ''\n        while value:\n            value, mod = divmod(value, 58)\n            txt += cls.chars[mod]\n\n        for byte in be_bytes:\n            if byte != 0:\n                break\n            txt += '1'\n\n        return txt[::-1]\n\n    @classmethod\n    def decode_check(cls, txt, hash_fn=double_sha256):\n        \"\"\" Decodes a Base58Check-encoded string to a payload. The version prefixes it. \"\"\"\n        be_bytes = cls.decode(txt)\n        result, check = be_bytes[:-4], be_bytes[-4:]\n        if check != hash_fn(result)[:4]:\n            raise Base58Error(f'invalid base 58 checksum for {txt}')\n        return result\n\n    @classmethod\n    def encode_check(cls, payload, hash_fn=double_sha256):\n        \"\"\" Encodes a payload bytearray (which includes the version byte(s))\n            into a Base58Check string.\"\"\"\n        be_bytes = payload + hash_fn(payload)[:4]\n        return cls.encode(be_bytes)\n"
  },
  {
    "path": "lbry/crypto/crypt.py",
    "content": "import os\nimport base64\nimport typing\nfrom cryptography.hazmat.primitives.kdf.scrypt import Scrypt\nfrom cryptography.hazmat.primitives.ciphers import Cipher, modes\nfrom cryptography.hazmat.primitives.ciphers.algorithms import AES\nfrom cryptography.hazmat.primitives.padding import PKCS7\nfrom cryptography.hazmat.backends import default_backend\n\nfrom lbry.error import InvalidPasswordError\nfrom lbry.crypto.hash import double_sha256\n\n\ndef aes_encrypt(secret: str, value: str, init_vector: bytes = None) -> str:\n    if init_vector is not None:\n        assert len(init_vector) == 16\n    else:\n        init_vector = os.urandom(16)\n    key = double_sha256(secret.encode())\n    encryptor = Cipher(AES(key), modes.CBC(init_vector), default_backend()).encryptor()\n    padder = PKCS7(AES.block_size).padder()\n    padded_data = padder.update(value.encode()) + padder.finalize()\n    encrypted_data = encryptor.update(padded_data) + encryptor.finalize()\n    return base64.b64encode(init_vector + encrypted_data).decode()\n\n\ndef aes_decrypt(secret: str, value: str) -> typing.Tuple[str, bytes]:\n    try:\n        data = base64.b64decode(value.encode())\n        key = double_sha256(secret.encode())\n        init_vector, data = data[:16], data[16:]\n        decryptor = Cipher(AES(key), modes.CBC(init_vector), default_backend()).decryptor()\n        unpadder = PKCS7(AES.block_size).unpadder()\n        result = unpadder.update(decryptor.update(data)) + unpadder.finalize()\n        return result.decode(), init_vector\n    except UnicodeDecodeError:\n        raise InvalidPasswordError()\n    except ValueError as e:\n        if e.args[0] == 'Invalid padding bytes.':\n            raise InvalidPasswordError()\n        raise\n\n\ndef better_aes_encrypt(secret: str, value: bytes) -> bytes:\n    init_vector = os.urandom(16)\n    key = scrypt(secret.encode(), salt=init_vector)\n    encryptor = Cipher(AES(key), modes.CBC(init_vector), default_backend()).encryptor()\n    padder = PKCS7(AES.block_size).padder()\n    padded_data = padder.update(value) + padder.finalize()\n    encrypted_data = encryptor.update(padded_data) + encryptor.finalize()\n    return base64.b64encode(b's:8192:16:1:' + init_vector + encrypted_data)\n\n\ndef better_aes_decrypt(secret: str, value: bytes) -> bytes:\n    try:\n        data = base64.b64decode(value)\n        _, scryp_n, scrypt_r, scrypt_p, data = data.split(b':', maxsplit=4)\n        init_vector, data = data[:16], data[16:]\n        key = scrypt(secret.encode(), init_vector, int(scryp_n), int(scrypt_r), int(scrypt_p))\n        decryptor = Cipher(AES(key), modes.CBC(init_vector), default_backend()).decryptor()\n        unpadder = PKCS7(AES.block_size).unpadder()\n        return unpadder.update(decryptor.update(data)) + unpadder.finalize()\n    except ValueError as e:\n        if e.args[0] == 'Invalid padding bytes.':\n            raise InvalidPasswordError()\n        raise\n\n\ndef scrypt(passphrase, salt, scrypt_n=1<<13, scrypt_r=16, scrypt_p=1):\n    kdf = Scrypt(salt, length=32, n=scrypt_n, r=scrypt_r, p=scrypt_p, backend=default_backend())\n    return kdf.derive(passphrase)\n"
  },
  {
    "path": "lbry/crypto/hash.py",
    "content": "import hashlib\nimport hmac\nfrom binascii import hexlify, unhexlify\n\n\ndef sha256(x):\n    \"\"\" Simple wrapper of hashlib sha256. \"\"\"\n    return hashlib.sha256(x).digest()\n\n\ndef sha512(x):\n    \"\"\" Simple wrapper of hashlib sha512. \"\"\"\n    return hashlib.sha512(x).digest()\n\n\ndef ripemd160(x):\n    \"\"\" Simple wrapper of hashlib ripemd160. \"\"\"\n    h = hashlib.new('ripemd160')\n    h.update(x)\n    return h.digest()\n\n\ndef double_sha256(x):\n    \"\"\" SHA-256 of SHA-256, as used extensively in bitcoin. \"\"\"\n    return sha256(sha256(x))\n\n\ndef hmac_sha512(key, msg):\n    \"\"\" Use SHA-512 to provide an HMAC. \"\"\"\n    return hmac.new(key, msg, hashlib.sha512).digest()\n\n\ndef hash160(x):\n    \"\"\" RIPEMD-160 of SHA-256.\n        Used to make bitcoin addresses from pubkeys. \"\"\"\n    return ripemd160(sha256(x))\n\n\ndef hash_to_hex_str(x):\n    \"\"\" Convert a big-endian binary hash to displayed hex string.\n        Display form of a binary hash is reversed and converted to hex. \"\"\"\n    return hexlify(reversed(x))\n\n\ndef hex_str_to_hash(x):\n    \"\"\" Convert a displayed hex string to a binary hash. \"\"\"\n    return reversed(unhexlify(x))\n"
  },
  {
    "path": "lbry/crypto/util.py",
    "content": "from binascii import unhexlify, hexlify\n\n\ndef bytes_to_int(be_bytes):\n    \"\"\" Interprets a big-endian sequence of bytes as an integer. \"\"\"\n    return int(hexlify(be_bytes), 16)\n\n\ndef int_to_bytes(value):\n    \"\"\" Converts an integer to a big-endian sequence of bytes. \"\"\"\n    length = (value.bit_length() + 7) // 8\n    s = '%x' % value\n    return unhexlify(('0' * (len(s) % 2) + s).zfill(length * 2))\n"
  },
  {
    "path": "lbry/dht/__init__.py",
    "content": ""
  },
  {
    "path": "lbry/dht/blob_announcer.py",
    "content": "import asyncio\nimport typing\nimport logging\n\nfrom prometheus_client import Counter, Gauge\n\nif typing.TYPE_CHECKING:\n    from lbry.dht.node import Node\n    from lbry.extras.daemon.storage import SQLiteStorage\n\nlog = logging.getLogger(__name__)\n\n\nclass BlobAnnouncer:\n    announcements_sent_metric = Counter(\n        \"announcements_sent\", \"Number of announcements sent and their respective status.\", namespace=\"dht_node\",\n        labelnames=(\"peers\", \"error\"),\n    )\n    announcement_queue_size_metric = Gauge(\n        \"announcement_queue_size\", \"Number of hashes waiting to be announced.\", namespace=\"dht_node\",\n        labelnames=(\"scope\",)\n    )\n\n    def __init__(self, loop: asyncio.AbstractEventLoop, node: 'Node', storage: 'SQLiteStorage'):\n        self.loop = loop\n        self.node = node\n        self.storage = storage\n        self.announce_task: asyncio.Task = None\n        self.announce_queue: typing.List[str] = []\n        self._done = asyncio.Event()\n        self.announced = set()\n\n    async def _run_consumer(self):\n        while self.announce_queue:\n            try:\n                blob_hash = self.announce_queue.pop()\n                peers = len(await self.node.announce_blob(blob_hash))\n                self.announcements_sent_metric.labels(peers=peers, error=False).inc()\n                if peers > 4:\n                    self.announced.add(blob_hash)\n                else:\n                    log.debug(\"failed to announce %s, could only find %d peers, retrying soon.\", blob_hash[:8], peers)\n            except Exception as err:\n                self.announcements_sent_metric.labels(peers=0, error=True).inc()\n                log.warning(\"error announcing %s: %s\", blob_hash[:8], str(err))\n\n    async def _announce(self, batch_size: typing.Optional[int] = 10):\n        while batch_size:\n            if not self.node.joined.is_set():\n                await self.node.joined.wait()\n            await asyncio.sleep(60)\n            if not self.node.protocol.routing_table.get_peers():\n                log.warning(\"No peers in DHT, announce round skipped\")\n                continue\n            self.announce_queue.extend(await self.storage.get_blobs_to_announce())\n            self.announcement_queue_size_metric.labels(scope=\"global\").set(len(self.announce_queue))\n            log.debug(\"announcer task wake up, %d blobs to announce\", len(self.announce_queue))\n            while len(self.announce_queue) > 0:\n                log.info(\"%i blobs to announce\", len(self.announce_queue))\n                await asyncio.gather(*[self._run_consumer() for _ in range(batch_size)])\n                announced = list(filter(None, self.announced))\n                if announced:\n                    await self.storage.update_last_announced_blobs(announced)\n                    log.info(\"announced %i blobs\", len(announced))\n                    self.announced.clear()\n            self._done.set()\n            self._done.clear()\n\n    def start(self, batch_size: typing.Optional[int] = 10):\n        assert not self.announce_task or self.announce_task.done(), \"already running\"\n        self.announce_task = self.loop.create_task(self._announce(batch_size))\n\n    def stop(self):\n        if self.announce_task and not self.announce_task.done():\n            self.announce_task.cancel()\n\n    def wait(self):\n        return self._done.wait()\n"
  },
  {
    "path": "lbry/dht/constants.py",
    "content": "import hashlib\nimport os\n\nHASH_CLASS = hashlib.sha384  # pylint: disable=invalid-name\nHASH_LENGTH = HASH_CLASS().digest_size\nHASH_BITS = HASH_LENGTH * 8\nALPHA = 5\nK = 8\nSPLIT_BUCKETS_UNDER_INDEX = 1\nREPLACEMENT_CACHE_SIZE = 8\nRPC_TIMEOUT = 5.0\nRPC_ATTEMPTS = 5\nRPC_ATTEMPTS_PRUNING_WINDOW = 600\nITERATIVE_LOOKUP_DELAY = RPC_TIMEOUT / 2.0  # TODO: use config val / 2 if rpc timeout is provided\nREFRESH_INTERVAL = 3600  # 1 hour\nREPLICATE_INTERVAL = REFRESH_INTERVAL\nDATA_EXPIRATION = 86400  # 24 hours\nTOKEN_SECRET_REFRESH_INTERVAL = 300  # 5 minutes\nMAYBE_PING_DELAY = 300  # 5 minutes\nCHECK_REFRESH_INTERVAL = REFRESH_INTERVAL / 5\nRPC_ID_LENGTH = 20\nPROTOCOL_VERSION = 1\nMSG_SIZE_LIMIT = 1400\n\n\ndef digest(data: bytes) -> bytes:\n    h = HASH_CLASS()\n    h.update(data)\n    return h.digest()\n\n\ndef generate_id(num=None) -> bytes:\n    if num is not None:\n        return digest(str(num).encode())\n    else:\n        return digest(os.urandom(32))\n\n\ndef generate_rpc_id(num=None) -> bytes:\n    return generate_id(num)[:RPC_ID_LENGTH]\n"
  },
  {
    "path": "lbry/dht/error.py",
    "content": "class BaseKademliaException(Exception):\n    pass\n\n\nclass DecodeError(BaseKademliaException):\n    \"\"\"\n    Should be raised by an C{Encoding} implementation if decode operation\n    fails\n    \"\"\"\n\n\nclass BucketFull(BaseKademliaException):\n    \"\"\"\n    Raised when the bucket is full\n    \"\"\"\n\n\nclass RemoteException(BaseKademliaException):\n    pass\n\n\nclass TransportNotConnected(BaseKademliaException):\n    pass\n"
  },
  {
    "path": "lbry/dht/node.py",
    "content": "import logging\nimport asyncio\nimport typing\nimport socket\n\nfrom prometheus_client import Gauge\n\nfrom lbry.utils import aclosing, resolve_host\nfrom lbry.dht import constants\nfrom lbry.dht.peer import make_kademlia_peer\nfrom lbry.dht.protocol.distance import Distance\nfrom lbry.dht.protocol.iterative_find import IterativeNodeFinder, IterativeValueFinder\nfrom lbry.dht.protocol.protocol import KademliaProtocol\n\nif typing.TYPE_CHECKING:\n    from lbry.dht.peer import PeerManager\n    from lbry.dht.peer import KademliaPeer\n\nlog = logging.getLogger(__name__)\n\n\nclass Node:\n    storing_peers_metric = Gauge(\n        \"storing_peers\", \"Number of peers storing blobs announced to this node\", namespace=\"dht_node\",\n        labelnames=(\"scope\",),\n    )\n    stored_blob_with_x_bytes_colliding = Gauge(\n        \"stored_blobs_x_bytes_colliding\", \"Number of blobs with at least X bytes colliding with this node id prefix\",\n        namespace=\"dht_node\", labelnames=(\"amount\",)\n    )\n    def __init__(self, loop: asyncio.AbstractEventLoop, peer_manager: 'PeerManager', node_id: bytes, udp_port: int,\n                 internal_udp_port: int, peer_port: int, external_ip: str, rpc_timeout: float = constants.RPC_TIMEOUT,\n                 split_buckets_under_index: int = constants.SPLIT_BUCKETS_UNDER_INDEX, is_bootstrap_node: bool = False,\n                 storage: typing.Optional['SQLiteStorage'] = None):\n        self.loop = loop\n        self.internal_udp_port = internal_udp_port\n        self.protocol = KademliaProtocol(loop, peer_manager, node_id, external_ip, udp_port, peer_port, rpc_timeout,\n                                         split_buckets_under_index, is_bootstrap_node)\n        self.listening_port: asyncio.DatagramTransport = None\n        self.joined = asyncio.Event()\n        self._join_task: asyncio.Task = None\n        self._refresh_task: asyncio.Task = None\n        self._storage = storage\n\n    @property\n    def stored_blob_hashes(self):\n        return self.protocol.data_store.keys()\n\n    async def refresh_node(self, force_once=False):\n        while True:\n            # remove peers with expired blob announcements from the datastore\n            self.protocol.data_store.removed_expired_peers()\n\n            total_peers: typing.List['KademliaPeer'] = []\n            # add all peers in the routing table\n            total_peers.extend(self.protocol.routing_table.get_peers())\n            # add all the peers who have announced blobs to us\n            storing_peers = self.protocol.data_store.get_storing_contacts()\n            self.storing_peers_metric.labels(\"global\").set(len(storing_peers))\n            total_peers.extend(storing_peers)\n\n            counts = {0: 0, 1: 0, 2: 0}\n            node_id = self.protocol.node_id\n            for blob_hash in self.protocol.data_store.keys():\n                bytes_colliding = 0 if blob_hash[0] != node_id[0] else 2 if blob_hash[1] == node_id[1] else 1\n                counts[bytes_colliding] += 1\n            self.stored_blob_with_x_bytes_colliding.labels(amount=0).set(counts[0])\n            self.stored_blob_with_x_bytes_colliding.labels(amount=1).set(counts[1])\n            self.stored_blob_with_x_bytes_colliding.labels(amount=2).set(counts[2])\n\n            # get ids falling in the midpoint of each bucket that hasn't been recently updated\n            node_ids = self.protocol.routing_table.get_refresh_list(0, True)\n\n            if self.protocol.routing_table.get_peers():\n                # if we have node ids to look up, perform the iterative search until we have k results\n                while node_ids:\n                    peers = await self.peer_search(node_ids.pop())\n                    total_peers.extend(peers)\n            else:\n                if force_once:\n                    break\n                fut = asyncio.Future()\n                self.loop.call_later(constants.REFRESH_INTERVAL // 4, fut.set_result, None)\n                await fut\n                continue\n\n            # ping the set of peers; upon success/failure the routing able and last replied/failed time will be updated\n            to_ping = [peer for peer in set(total_peers) if self.protocol.peer_manager.peer_is_good(peer) is not True]\n            if to_ping:\n                self.protocol.ping_queue.enqueue_maybe_ping(*to_ping, delay=0)\n            if self._storage:\n                await self._storage.save_kademlia_peers(self.protocol.routing_table.get_peers())\n            if force_once:\n                break\n\n            fut = asyncio.Future()\n            self.loop.call_later(constants.REFRESH_INTERVAL, fut.set_result, None)\n            await fut\n\n    async def announce_blob(self, blob_hash: str) -> typing.List[bytes]:\n        hash_value = bytes.fromhex(blob_hash)\n        assert len(hash_value) == constants.HASH_LENGTH\n        peers = await self.peer_search(hash_value)\n\n        if not self.protocol.external_ip:\n            raise Exception(\"Cannot determine external IP\")\n        log.debug(\"Store to %i peers\", len(peers))\n        for peer in peers:\n            log.debug(\"store to %s %s %s\", peer.address, peer.udp_port, peer.tcp_port)\n        stored_to_tup = await asyncio.gather(\n            *(self.protocol.store_to_peer(hash_value, peer) for peer in peers)\n        )\n        stored_to = [node_id for node_id, contacted in stored_to_tup if contacted]\n        if stored_to:\n            log.debug(\n                \"Stored %s to %i of %i attempted peers\", hash_value.hex()[:8],\n                len(stored_to), len(peers)\n            )\n        else:\n            log.debug(\"Failed announcing %s, stored to 0 peers\", blob_hash[:8])\n        return stored_to\n\n    def stop(self) -> None:\n        if self.joined.is_set():\n            self.joined.clear()\n        if self._join_task:\n            self._join_task.cancel()\n        if self._refresh_task and not (self._refresh_task.done() or self._refresh_task.cancelled()):\n            self._refresh_task.cancel()\n        if self.protocol and self.protocol.ping_queue.running:\n            self.protocol.ping_queue.stop()\n            self.protocol.stop()\n        if self.listening_port is not None:\n            self.listening_port.close()\n        self._join_task = None\n        self.listening_port = None\n        log.info(\"Stopped DHT node\")\n\n    async def start_listening(self, interface: str = '0.0.0.0') -> None:\n        if not self.listening_port:\n            self.listening_port, _ = await self.loop.create_datagram_endpoint(\n                lambda: self.protocol, (interface, self.internal_udp_port)\n            )\n            log.info(\"DHT node listening on UDP %s:%i\", interface, self.internal_udp_port)\n            self.protocol.start()\n        else:\n            log.warning(\"Already bound to port %s\", self.listening_port)\n\n    async def join_network(self, interface: str = '0.0.0.0',\n                           known_node_urls: typing.Optional[typing.List[typing.Tuple[str, int]]] = None):\n        def peers_from_urls(urls: typing.Optional[typing.List[typing.Tuple[bytes, str, int, int]]]):\n            peer_addresses = []\n            for node_id, address, udp_port, tcp_port in urls:\n                if (node_id, address, udp_port, tcp_port) not in peer_addresses and \\\n                        (address, udp_port) != (self.protocol.external_ip, self.protocol.udp_port):\n                    peer_addresses.append((node_id, address, udp_port, tcp_port))\n            return [make_kademlia_peer(*peer_address) for peer_address in peer_addresses]\n\n        if not self.listening_port:\n            await self.start_listening(interface)\n        self.protocol.ping_queue.start()\n        self._refresh_task = self.loop.create_task(self.refresh_node())\n\n        while True:\n            if self.protocol.routing_table.get_peers():\n                if not self.joined.is_set():\n                    self.joined.set()\n                    log.info(\n                        \"joined dht, %i peers known in %i buckets\", len(self.protocol.routing_table.get_peers()),\n                        self.protocol.routing_table.buckets_with_contacts()\n                    )\n            else:\n                if self.joined.is_set():\n                    self.joined.clear()\n                seed_peers = peers_from_urls(\n                    await self._storage.get_persisted_kademlia_peers()\n                ) if self._storage else []\n                if not seed_peers:\n                    try:\n                        seed_peers.extend(peers_from_urls([\n                            (None, await resolve_host(address, udp_port, 'udp'), udp_port, None)\n                            for address, udp_port in known_node_urls or []\n                        ]))\n                    except socket.gaierror:\n                        await asyncio.sleep(30)\n                        continue\n\n                self.protocol.peer_manager.reset()\n                self.protocol.ping_queue.enqueue_maybe_ping(*seed_peers, delay=0.0)\n                await self.peer_search(self.protocol.node_id, shortlist=seed_peers, count=32)\n\n            await asyncio.sleep(1)\n\n    def start(self, interface: str, known_node_urls: typing.Optional[typing.List[typing.Tuple[str, int]]] = None):\n        self._join_task = self.loop.create_task(self.join_network(interface, known_node_urls))\n\n    def get_iterative_node_finder(self, key: bytes, shortlist: typing.Optional[typing.List['KademliaPeer']] = None,\n                                  max_results: int = constants.K) -> IterativeNodeFinder:\n        shortlist = shortlist or self.protocol.routing_table.find_close_peers(key)\n        return IterativeNodeFinder(self.loop, self.protocol, key, max_results, shortlist)\n\n    def get_iterative_value_finder(self, key: bytes, shortlist: typing.Optional[typing.List['KademliaPeer']] = None,\n                                   max_results: int = -1) -> IterativeValueFinder:\n        shortlist = shortlist or self.protocol.routing_table.find_close_peers(key)\n        return IterativeValueFinder(self.loop, self.protocol, key, max_results, shortlist)\n\n    async def peer_search(self, node_id: bytes, count=constants.K, max_results=constants.K * 2,\n                          shortlist: typing.Optional[typing.List['KademliaPeer']] = None\n                          ) -> typing.List['KademliaPeer']:\n        peers = []\n        async with aclosing(self.get_iterative_node_finder(\n                node_id, shortlist=shortlist, max_results=max_results)) as node_finder:\n            async for iteration_peers in node_finder:\n                peers.extend(iteration_peers)\n        distance = Distance(node_id)\n        peers.sort(key=lambda peer: distance(peer.node_id))\n        return peers[:count]\n\n    async def _accumulate_peers_for_value(self, search_queue: asyncio.Queue, result_queue: asyncio.Queue):\n        tasks = []\n        try:\n            while True:\n                blob_hash = await search_queue.get()\n                tasks.append(self.loop.create_task(self._peers_for_value_producer(blob_hash, result_queue)))\n        finally:\n            for task in tasks:\n                task.cancel()\n\n    async def _peers_for_value_producer(self, blob_hash: str, result_queue: asyncio.Queue):\n        async def put_into_result_queue_after_pong(_peer):\n            try:\n                await self.protocol.get_rpc_peer(_peer).ping()\n                result_queue.put_nowait([_peer])\n                log.debug(\"pong from %s:%i for %s\", _peer.address, _peer.udp_port, blob_hash)\n            except asyncio.TimeoutError:\n                pass\n\n        # prioritize peers who reply to a dht ping first\n        # this minimizes attempting to make tcp connections that won't work later to dead or unreachable peers\n        async with aclosing(self.get_iterative_value_finder(bytes.fromhex(blob_hash))) as value_finder:\n            async for results in value_finder:\n                to_put = []\n                for peer in results:\n                    if peer.address == self.protocol.external_ip and self.protocol.peer_port == peer.tcp_port:\n                        continue\n                    is_good = self.protocol.peer_manager.peer_is_good(peer)\n                    if is_good:\n                        # the peer has replied recently over UDP, it can probably be reached on the TCP port\n                        to_put.append(peer)\n                    elif is_good is None:\n                        if not peer.udp_port:\n                            # TODO: use the same port for TCP and UDP\n                            # the udp port must be guessed\n                            # default to the ports being the same. if the TCP port appears to be <=0.48.0 default,\n                            # including on a network with several nodes, then assume the udp port is proportionately\n                            # based on a starting port of 4444\n                            udp_port_to_try = peer.tcp_port\n                            if 3400 > peer.tcp_port > 3332:\n                                udp_port_to_try = (peer.tcp_port - 3333) + 4444\n                            self.loop.create_task(put_into_result_queue_after_pong(\n                                make_kademlia_peer(peer.node_id, peer.address, udp_port_to_try, peer.tcp_port)\n                            ))\n                        else:\n                            self.loop.create_task(put_into_result_queue_after_pong(peer))\n                    else:\n                        # the peer is known to be bad/unreachable, skip trying to connect to it over TCP\n                        log.debug(\"skip bad peer %s:%i for %s\", peer.address, peer.tcp_port, blob_hash)\n                if to_put:\n                    result_queue.put_nowait(to_put)\n\n    def accumulate_peers(self, search_queue: asyncio.Queue,\n                         peer_queue: typing.Optional[asyncio.Queue] = None\n                         ) -> typing.Tuple[asyncio.Queue, asyncio.Task]:\n        queue = peer_queue or asyncio.Queue()\n        return queue, self.loop.create_task(self._accumulate_peers_for_value(search_queue, queue))\n\n\nasync def get_kademlia_peers_from_hosts(peer_list: typing.List[typing.Tuple[str, int]]) -> typing.List['KademliaPeer']:\n    peer_address_list = [(await resolve_host(url, port, proto='tcp'), port) for url, port in peer_list]\n    kademlia_peer_list = [make_kademlia_peer(None, address, None, tcp_port=port, allow_localhost=True)\n                          for address, port in peer_address_list]\n    return kademlia_peer_list\n"
  },
  {
    "path": "lbry/dht/peer.py",
    "content": "import typing\nimport asyncio\nimport logging\nfrom dataclasses import dataclass, field\nfrom functools import lru_cache\n\nfrom prometheus_client import Gauge\n\nfrom lbry.utils import is_valid_public_ipv4 as _is_valid_public_ipv4, LRUCache\nfrom lbry.dht import constants\nfrom lbry.dht.serialization.datagram import make_compact_address, make_compact_ip, decode_compact_address\n\nALLOW_LOCALHOST = False\nCACHE_SIZE = 16384\nlog = logging.getLogger(__name__)\n\n\n@lru_cache(CACHE_SIZE)\ndef make_kademlia_peer(node_id: typing.Optional[bytes], address: typing.Optional[str],\n                       udp_port: typing.Optional[int] = None,\n                       tcp_port: typing.Optional[int] = None,\n                       allow_localhost: bool = False) -> 'KademliaPeer':\n    return KademliaPeer(address, node_id, udp_port, tcp_port=tcp_port, allow_localhost=allow_localhost)\n\n\ndef is_valid_public_ipv4(address, allow_localhost: bool = False):\n    allow_localhost = bool(allow_localhost or ALLOW_LOCALHOST)\n    return _is_valid_public_ipv4(address, allow_localhost)\n\n\nclass PeerManager:\n    peer_manager_keys_metric = Gauge(\n        \"peer_manager_keys\", \"Number of keys tracked by PeerManager dicts (sum)\", namespace=\"dht_node\",\n        labelnames=(\"scope\",)\n    )\n    def __init__(self, loop: asyncio.AbstractEventLoop):\n        self._loop = loop\n        self._rpc_failures: typing.Dict[\n            typing.Tuple[str, int], typing.Tuple[typing.Optional[float], typing.Optional[float]]\n        ] = LRUCache(CACHE_SIZE)\n        self._last_replied: typing.Dict[typing.Tuple[str, int], float] = LRUCache(CACHE_SIZE)\n        self._last_sent: typing.Dict[typing.Tuple[str, int], float] = LRUCache(CACHE_SIZE)\n        self._last_requested: typing.Dict[typing.Tuple[str, int], float] = LRUCache(CACHE_SIZE)\n        self._node_id_mapping: typing.Dict[typing.Tuple[str, int], bytes] = LRUCache(CACHE_SIZE)\n        self._node_id_reverse_mapping: typing.Dict[bytes, typing.Tuple[str, int]] = LRUCache(CACHE_SIZE)\n        self._node_tokens: typing.Dict[bytes, (float, bytes)] = LRUCache(CACHE_SIZE)\n\n    def count_cache_keys(self):\n        return len(self._rpc_failures) + len(self._last_replied) + len(self._last_sent) + len(\n            self._last_requested) + len(self._node_id_mapping) + len(self._node_id_reverse_mapping) + len(\n            self._node_tokens)\n\n    def reset(self):\n        for statistic in (self._rpc_failures, self._last_replied, self._last_sent, self._last_requested):\n            statistic.clear()\n\n    def report_failure(self, address: str, udp_port: int):\n        now = self._loop.time()\n        _, previous = self._rpc_failures.pop((address, udp_port), (None, None))\n        self._rpc_failures[(address, udp_port)] = (previous, now)\n\n    def report_last_sent(self, address: str, udp_port: int):\n        now = self._loop.time()\n        self._last_sent[(address, udp_port)] = now\n\n    def report_last_replied(self, address: str, udp_port: int):\n        now = self._loop.time()\n        self._last_replied[(address, udp_port)] = now\n\n    def report_last_requested(self, address: str, udp_port: int):\n        now = self._loop.time()\n        self._last_requested[(address, udp_port)] = now\n\n    def clear_token(self, node_id: bytes):\n        self._node_tokens.pop(node_id, None)\n\n    def update_token(self, node_id: bytes, token: bytes):\n        now = self._loop.time()\n        self._node_tokens[node_id] = (now, token)\n\n    def get_node_token(self, node_id: bytes) -> typing.Optional[bytes]:\n        ts, token = self._node_tokens.get(node_id, (0, None))\n        if ts and ts > self._loop.time() - constants.TOKEN_SECRET_REFRESH_INTERVAL:\n            return token\n\n    def get_last_replied(self, address: str, udp_port: int) -> typing.Optional[float]:\n        return self._last_replied.get((address, udp_port))\n\n    def update_contact_triple(self, node_id: bytes, address: str, udp_port: int):\n        \"\"\"\n        Update the mapping of node_id -> address tuple and that of address tuple -> node_id\n        This is to handle peers changing addresses and ids while assuring that the we only ever have\n        one node id / address tuple mapped to each other\n        \"\"\"\n        if (address, udp_port) in self._node_id_mapping:\n            self._node_id_reverse_mapping.pop(self._node_id_mapping.pop((address, udp_port)))\n        if node_id in self._node_id_reverse_mapping:\n            self._node_id_mapping.pop(self._node_id_reverse_mapping.pop(node_id))\n        self._node_id_mapping[(address, udp_port)] = node_id\n        self._node_id_reverse_mapping[node_id] = (address, udp_port)\n        self.peer_manager_keys_metric.labels(\"global\").set(self.count_cache_keys())\n\n    def get_node_id_for_endpoint(self, address, port):\n        return self._node_id_mapping.get((address, port))\n\n    def prune(self):  # TODO: periodically call this\n        now = self._loop.time()\n        to_pop = []\n        for (address, udp_port), (_, last_failure) in self._rpc_failures.items():\n            if last_failure and last_failure < now - constants.RPC_ATTEMPTS_PRUNING_WINDOW:\n                to_pop.append((address, udp_port))\n        while to_pop:\n            del self._rpc_failures[to_pop.pop()]\n        to_pop = []\n        for node_id, (age, token) in self._node_tokens.items():  # pylint: disable=unused-variable\n            if age < now - constants.TOKEN_SECRET_REFRESH_INTERVAL:\n                to_pop.append(node_id)\n        while to_pop:\n            del self._node_tokens[to_pop.pop()]\n\n    def contact_triple_is_good(self, node_id: bytes, address: str, udp_port: int):  # pylint: disable=too-many-return-statements\n        \"\"\"\n        :return: False if peer is bad, None if peer is unknown, or True if peer is good\n        \"\"\"\n\n        delay = self._loop.time() - constants.CHECK_REFRESH_INTERVAL\n\n        # fixme: find a way to re-enable that without breaking other parts\n        # if node_id not in self._node_id_reverse_mapping or (address, udp_port) not in self._node_id_mapping:\n        #    return\n        # addr_tup = (address, udp_port)\n        # if self._node_id_reverse_mapping[node_id] != addr_tup or self._node_id_mapping[addr_tup] != node_id:\n        #    return\n        previous_failure, most_recent_failure = self._rpc_failures.get((address, udp_port), (None, None))\n        last_requested = self._last_requested.get((address, udp_port))\n        last_replied = self._last_replied.get((address, udp_port))\n        if node_id is None:\n            return None\n        if most_recent_failure and last_replied:\n            if delay < last_replied > most_recent_failure:\n                return True\n            elif last_replied > most_recent_failure:\n                return\n            return False\n        elif previous_failure and most_recent_failure and most_recent_failure > delay:\n            return False\n        elif last_replied and last_replied > delay:\n            return True\n        elif last_requested and last_requested > delay:\n            return None\n        return\n\n    def peer_is_good(self, peer: 'KademliaPeer'):\n        return self.contact_triple_is_good(peer.node_id, peer.address, peer.udp_port)\n\n\ndef decode_tcp_peer_from_compact_address(compact_address: bytes) -> 'KademliaPeer':  # pylint: disable=no-self-use\n    node_id, address, tcp_port = decode_compact_address(compact_address)\n    return make_kademlia_peer(node_id, address, udp_port=None, tcp_port=tcp_port)\n\n\n@dataclass(unsafe_hash=True)\nclass KademliaPeer:\n    address: str = field(hash=True)\n    _node_id: typing.Optional[bytes] = field(hash=True)\n    udp_port: typing.Optional[int] = field(hash=True)\n    tcp_port: typing.Optional[int] = field(compare=False, hash=False)\n    protocol_version: typing.Optional[int] = field(default=1, compare=False, hash=False)\n    allow_localhost: bool = field(default=False, compare=False, hash=False)\n\n    def __post_init__(self):\n        if self._node_id is not None:\n            if not len(self._node_id) == constants.HASH_LENGTH:\n                raise ValueError(\"invalid node_id: {}\".format(self._node_id.hex()))\n        if self.udp_port is not None and not 1024 <= self.udp_port <= 65535:\n            raise ValueError(f\"invalid udp port: {self.address}:{self.udp_port}\")\n        if self.tcp_port is not None and not 1024 <= self.tcp_port <= 65535:\n            raise ValueError(f\"invalid tcp port: {self.address}:{self.tcp_port}\")\n        if not is_valid_public_ipv4(self.address, self.allow_localhost):\n            raise ValueError(f\"invalid ip address: '{self.address}'\")\n\n    def update_tcp_port(self, tcp_port: int):\n        self.tcp_port = tcp_port\n\n    @property\n    def node_id(self) -> bytes:\n        return self._node_id\n\n    def compact_address_udp(self) -> bytearray:\n        return make_compact_address(self.node_id, self.address, self.udp_port)\n\n    def compact_address_tcp(self) -> bytearray:\n        return make_compact_address(self.node_id, self.address, self.tcp_port)\n\n    def compact_ip(self):\n        return make_compact_ip(self.address)\n\n    def __str__(self):\n        return f\"{self.__class__.__name__}({self.node_id.hex()[:8]}@{self.address}:{self.udp_port}-{self.tcp_port})\"\n"
  },
  {
    "path": "lbry/dht/protocol/__init__.py",
    "content": ""
  },
  {
    "path": "lbry/dht/protocol/data_store.py",
    "content": "import asyncio\nimport typing\n\nfrom lbry.dht import constants\nif typing.TYPE_CHECKING:\n    from lbry.dht.peer import KademliaPeer, PeerManager\n\n\nclass DictDataStore:\n    def __init__(self, loop: asyncio.AbstractEventLoop, peer_manager: 'PeerManager'):\n        # Dictionary format:\n        # { <key>: [(<contact>, <age>), ...] }\n        self._data_store: typing.Dict[bytes, typing.List[typing.Tuple['KademliaPeer', float]]] = {}\n\n        self.loop = loop\n        self._peer_manager = peer_manager\n        self.completed_blobs: typing.Set[str] = set()\n\n    def keys(self):\n        return self._data_store.keys()\n\n    def __len__(self):\n        return self._data_store.__len__()\n\n    def removed_expired_peers(self):\n        now = self.loop.time()\n        keys = list(self._data_store.keys())\n        for key in keys:\n            to_remove = []\n            for (peer, ts) in self._data_store[key]:\n                if ts + constants.DATA_EXPIRATION < now or self._peer_manager.peer_is_good(peer) is False:\n                    to_remove.append((peer, ts))\n            for item in to_remove:\n                self._data_store[key].remove(item)\n            if not self._data_store[key]:\n                del self._data_store[key]\n\n    def filter_bad_and_expired_peers(self, key: bytes) -> typing.Iterator['KademliaPeer']:\n        \"\"\"\n        Returns only non-expired and unknown/good peers\n        \"\"\"\n        for peer in self.filter_expired_peers(key):\n            if self._peer_manager.peer_is_good(peer) is not False:\n                yield peer\n\n    def filter_expired_peers(self, key: bytes) -> typing.Iterator['KademliaPeer']:\n        \"\"\"\n        Returns only non-expired peers\n        \"\"\"\n        now = self.loop.time()\n        for (peer, ts) in self._data_store.get(key, []):\n            if ts + constants.DATA_EXPIRATION > now:\n                yield peer\n\n    def has_peers_for_blob(self, key: bytes) -> bool:\n        return key in self._data_store\n\n    def add_peer_to_blob(self, contact: 'KademliaPeer', key: bytes) -> None:\n        now = self.loop.time()\n        if key in self._data_store:\n            current = list(filter(lambda x: x[0] == contact, self._data_store[key]))\n            if len(current) > 0:\n                self._data_store[key][self._data_store[key].index(current[0])] = contact, now\n            else:\n                self._data_store[key].append((contact, now))\n        else:\n            self._data_store[key] = [(contact, now)]\n\n    def get_peers_for_blob(self, key: bytes) -> typing.List['KademliaPeer']:\n        return list(self.filter_bad_and_expired_peers(key))\n\n    def get_storing_contacts(self) -> typing.List['KademliaPeer']:\n        peers = set()\n        for _, stored in self._data_store.items():\n            peers.update(set(map(lambda tup: tup[0], stored)))\n        return list(peers)\n"
  },
  {
    "path": "lbry/dht/protocol/distance.py",
    "content": "from lbry.dht import constants\n\n\nclass Distance:\n    \"\"\"Calculate the XOR result between two string variables.\n\n    Frequently we re-use one of the points so as an optimization\n    we pre-calculate the value of that point.\n    \"\"\"\n\n    def __init__(self, key: bytes):\n        if len(key) != constants.HASH_LENGTH:\n            raise ValueError(f\"invalid key length: {len(key)}\")\n        self.key = key\n        self.val_key_one = int.from_bytes(key, 'big')\n\n    def __call__(self, key_two: bytes) -> int:\n        if len(key_two) != constants.HASH_LENGTH:\n            raise ValueError(f\"invalid length of key to compare: {len(key_two)}\")\n        val_key_two = int.from_bytes(key_two, 'big')\n        return self.val_key_one ^ val_key_two\n\n    def is_closer(self, key_a: bytes, key_b: bytes) -> bool:\n        \"\"\"Returns true is `key_a` is closer to `key` than `key_b` is\"\"\"\n        return self(key_a) < self(key_b)\n"
  },
  {
    "path": "lbry/dht/protocol/iterative_find.py",
    "content": "import asyncio\nfrom itertools import chain\nfrom collections import defaultdict, OrderedDict\nfrom collections.abc import AsyncIterator\nimport typing\nimport logging\nfrom typing import TYPE_CHECKING\nfrom lbry.dht import constants\nfrom lbry.dht.error import RemoteException, TransportNotConnected\nfrom lbry.dht.protocol.distance import Distance\nfrom lbry.dht.peer import make_kademlia_peer, decode_tcp_peer_from_compact_address\nfrom lbry.dht.serialization.datagram import PAGE_KEY\n\nif TYPE_CHECKING:\n    from lbry.dht.protocol.protocol import KademliaProtocol\n    from lbry.dht.peer import PeerManager, KademliaPeer\n\nlog = logging.getLogger(__name__)\n\n\nclass FindResponse:\n    @property\n    def found(self) -> bool:\n        raise NotImplementedError()\n\n    def get_close_triples(self) -> typing.List[typing.Tuple[bytes, str, int]]:\n        raise NotImplementedError()\n\n    def get_close_kademlia_peers(self, peer_info) -> typing.Generator[typing.Iterator['KademliaPeer'], None, None]:\n        for contact_triple in self.get_close_triples():\n            node_id, address, udp_port = contact_triple\n            try:\n                yield make_kademlia_peer(node_id, address, udp_port)\n            except ValueError:\n                log.warning(\"misbehaving peer %s:%i returned peer with reserved ip %s:%i\", peer_info.address,\n                            peer_info.udp_port, address, udp_port)\n\n\nclass FindNodeResponse(FindResponse):\n    def __init__(self, key: bytes, close_triples: typing.List[typing.Tuple[bytes, str, int]]):\n        self.key = key\n        self.close_triples = close_triples\n\n    @property\n    def found(self) -> bool:\n        return self.key in [triple[0] for triple in self.close_triples]\n\n    def get_close_triples(self) -> typing.List[typing.Tuple[bytes, str, int]]:\n        return self.close_triples\n\n\nclass FindValueResponse(FindResponse):\n    def __init__(self, key: bytes, result_dict: typing.Dict):\n        self.key = key\n        self.token = result_dict[b'token']\n        self.close_triples: typing.List[typing.Tuple[bytes, bytes, int]] = result_dict.get(b'contacts', [])\n        self.found_compact_addresses = result_dict.get(key, [])\n        self.pages = int(result_dict.get(PAGE_KEY, 0))\n\n    @property\n    def found(self) -> bool:\n        return len(self.found_compact_addresses) > 0\n\n    def get_close_triples(self) -> typing.List[typing.Tuple[bytes, str, int]]:\n        return [(node_id, address.decode(), port) for node_id, address, port in self.close_triples]\n\n\nclass IterativeFinder(AsyncIterator):\n    def __init__(self, loop: asyncio.AbstractEventLoop,\n                 protocol: 'KademliaProtocol', key: bytes,\n                 max_results: typing.Optional[int] = constants.K,\n                 shortlist: typing.Optional[typing.List['KademliaPeer']] = None):\n        if len(key) != constants.HASH_LENGTH:\n            raise ValueError(\"invalid key length: %i\" % len(key))\n        self.loop = loop\n        self.peer_manager = protocol.peer_manager\n        self.protocol = protocol\n\n        self.key = key\n        self.max_results = max(constants.K, max_results)\n\n        self.active: typing.Dict['KademliaPeer', int] = OrderedDict()  # peer: distance, sorted\n        self.contacted: typing.Set['KademliaPeer'] = set()\n        self.distance = Distance(key)\n\n        self.iteration_queue = asyncio.Queue()\n\n        self.running_probes: typing.Dict['KademliaPeer', asyncio.Task] = {}\n        self.iteration_count = 0\n        self.running = False\n        self.tasks: typing.List[asyncio.Task] = []\n        for peer in shortlist:\n            if peer.node_id:\n                self._add_active(peer, force=True)\n            else:\n                # seed nodes\n                self._schedule_probe(peer)\n\n    async def send_probe(self, peer: 'KademliaPeer') -> FindResponse:\n        \"\"\"\n        Send the rpc request to the peer and return an object with the FindResponse interface\n        \"\"\"\n        raise NotImplementedError()\n\n    def search_exhausted(self):\n        \"\"\"\n        This method ends the iterator due no more peers to contact.\n        Override to provide last time results.\n        \"\"\"\n        self.iteration_queue.put_nowait(None)\n\n    def check_result_ready(self, response: FindResponse):\n        \"\"\"\n        Called after adding peers from an rpc result to the shortlist.\n        This method is responsible for putting a result for the generator into the Queue\n        \"\"\"\n        raise NotImplementedError()\n\n    def get_initial_result(self) -> typing.List['KademliaPeer']:  #pylint: disable=no-self-use\n        \"\"\"\n        Get an initial or cached result to be put into the Queue. Used for findValue requests where the blob\n        has peers in the local data store of blobs announced to us\n        \"\"\"\n        return []\n\n    def _add_active(self, peer, force=False):\n        if not force and self.peer_manager.peer_is_good(peer) is False:\n            return\n        if peer in self.contacted:\n            return\n        if peer not in self.active and peer.node_id and peer.node_id != self.protocol.node_id:\n            self.active[peer] = self.distance(peer.node_id)\n            self.active = OrderedDict(sorted(self.active.items(), key=lambda item: item[1]))\n\n    async def _handle_probe_result(self, peer: 'KademliaPeer', response: FindResponse):\n        self._add_active(peer)\n        for new_peer in response.get_close_kademlia_peers(peer):\n            self._add_active(new_peer)\n        self.check_result_ready(response)\n        self._log_state(reason=\"check result\")\n\n    def _reset_closest(self, peer):\n        if peer in self.active:\n            del self.active[peer]\n\n    async def _send_probe(self, peer: 'KademliaPeer'):\n        try:\n            response = await self.send_probe(peer)\n        except asyncio.TimeoutError:\n            self._reset_closest(peer)\n            return\n        except asyncio.CancelledError:\n            log.debug(\"%s[%x] cancelled probe\",\n                      type(self).__name__, id(self))\n            raise\n        except ValueError as err:\n            log.warning(str(err))\n            self._reset_closest(peer)\n            return\n        except TransportNotConnected:\n            await self._aclose(reason=\"not connected\")\n            return\n        except RemoteException:\n            self._reset_closest(peer)\n            return\n        return await self._handle_probe_result(peer, response)\n\n    def _search_round(self):\n        \"\"\"\n        Send up to constants.alpha (5) probes to closest active peers\n        \"\"\"\n\n        added = 0\n        for index, peer in enumerate(self.active.keys()):\n            if index == 0:\n                log.debug(\"%s[%x] closest to probe: %s\",\n                          type(self).__name__, id(self),\n                          peer.node_id.hex()[:8])\n            if peer in self.contacted:\n                continue\n            if len(self.running_probes) >= constants.ALPHA:\n                break\n            if index > (constants.K + len(self.running_probes)):\n                break\n            origin_address = (peer.address, peer.udp_port)\n            if peer.node_id == self.protocol.node_id:\n                continue\n            if origin_address == (self.protocol.external_ip, self.protocol.udp_port):\n                continue\n            self._schedule_probe(peer)\n            added += 1\n        log.debug(\"%s[%x] running %d probes for key %s\",\n                  type(self).__name__, id(self),\n                  len(self.running_probes), self.key.hex()[:8])\n        if not added and not self.running_probes:\n            log.debug(\"%s[%x] search for %s exhausted\",\n                      type(self).__name__, id(self),\n                      self.key.hex()[:8])\n            self.search_exhausted()\n\n    def _schedule_probe(self, peer: 'KademliaPeer'):\n        self.contacted.add(peer)\n\n        t = self.loop.create_task(self._send_probe(peer))\n\n        def callback(_):\n            self.running_probes.pop(peer, None)\n            if self.running:\n                self._search_round()\n\n        t.add_done_callback(callback)\n        self.running_probes[peer] = t\n\n    def _log_state(self, reason=\"?\"):\n        log.debug(\"%s[%x] [%s] %s: %i active nodes %i contacted %i produced %i queued\",\n                  type(self).__name__, id(self), self.key.hex()[:8],\n                  reason, len(self.active), len(self.contacted),\n                  self.iteration_count, self.iteration_queue.qsize())\n\n    def __aiter__(self):\n        if self.running:\n            raise Exception(\"already running\")\n        self.running = True\n        self.loop.call_soon(self._search_round)\n        return self\n\n    async def __anext__(self) -> typing.List['KademliaPeer']:\n        try:\n            if self.iteration_count == 0:\n                result = self.get_initial_result() or await self.iteration_queue.get()\n            else:\n                result = await self.iteration_queue.get()\n            if not result:\n                raise StopAsyncIteration\n            self.iteration_count += 1\n            return result\n        except asyncio.CancelledError:\n            await self._aclose(reason=\"cancelled\")\n            raise\n        except StopAsyncIteration:\n            await self._aclose(reason=\"no more results\")\n            raise\n\n    async def _aclose(self, reason=\"?\"):\n        log.debug(\"%s[%x] [%s] shutdown because %s: %i active nodes %i contacted %i produced %i queued\",\n                  type(self).__name__, id(self), self.key.hex()[:8],\n                  reason, len(self.active), len(self.contacted),\n                  self.iteration_count, self.iteration_queue.qsize())\n        self.running = False\n        self.iteration_queue.put_nowait(None)\n        for task in chain(self.tasks, self.running_probes.values()):\n            task.cancel()\n        self.tasks.clear()\n        self.running_probes.clear()\n\n    async def aclose(self):\n        if self.running:\n            await self._aclose(reason=\"aclose\")\n        log.debug(\"%s[%x] [%s] async close completed\",\n                  type(self).__name__, id(self), self.key.hex()[:8])\n\nclass IterativeNodeFinder(IterativeFinder):\n    def __init__(self, loop: asyncio.AbstractEventLoop,\n                 protocol: 'KademliaProtocol', key: bytes,\n                 max_results: typing.Optional[int] = constants.K,\n                 shortlist: typing.Optional[typing.List['KademliaPeer']] = None):\n        super().__init__(loop, protocol, key, max_results, shortlist)\n        self.yielded_peers: typing.Set['KademliaPeer'] = set()\n\n    async def send_probe(self, peer: 'KademliaPeer') -> FindNodeResponse:\n        log.debug(\"probe %s:%d (%s) for NODE %s\",\n                  peer.address, peer.udp_port, peer.node_id.hex()[:8] if peer.node_id else '', self.key.hex()[:8])\n        response = await self.protocol.get_rpc_peer(peer).find_node(self.key)\n        return FindNodeResponse(self.key, response)\n\n    def search_exhausted(self):\n        self.put_result(self.active.keys(), finish=True)\n\n    def put_result(self, from_iter: typing.Iterable['KademliaPeer'], finish=False):\n        not_yet_yielded = [\n            peer for peer in from_iter\n            if peer not in self.yielded_peers\n            and peer.node_id != self.protocol.node_id\n            and self.peer_manager.peer_is_good(peer) is True  # return only peers who answered\n        ]\n        not_yet_yielded.sort(key=lambda peer: self.distance(peer.node_id))\n        to_yield = not_yet_yielded[:max(constants.K, self.max_results)]\n        if to_yield:\n            self.yielded_peers.update(to_yield)\n            self.iteration_queue.put_nowait(to_yield)\n        if finish:\n            self.iteration_queue.put_nowait(None)\n\n    def check_result_ready(self, response: FindNodeResponse):\n        found = response.found and self.key != self.protocol.node_id\n\n        if found:\n            log.debug(\"found\")\n            return self.put_result(self.active.keys(), finish=True)\n\n\nclass IterativeValueFinder(IterativeFinder):\n    def __init__(self, loop: asyncio.AbstractEventLoop,\n                 protocol: 'KademliaProtocol', key: bytes,\n                 max_results: typing.Optional[int] = constants.K,\n                 shortlist: typing.Optional[typing.List['KademliaPeer']] = None):\n        super().__init__(loop, protocol, key, max_results, shortlist)\n        self.blob_peers: typing.Set['KademliaPeer'] = set()\n        # this tracks the index of the most recent page we requested from each peer\n        self.peer_pages: typing.DefaultDict['KademliaPeer', int] = defaultdict(int)\n        # this tracks the set of blob peers returned by each peer\n        self.discovered_peers: typing.Dict['KademliaPeer', typing.Set['KademliaPeer']] = defaultdict(set)\n\n    async def send_probe(self, peer: 'KademliaPeer') -> FindValueResponse:\n        log.debug(\"probe %s:%d (%s) for VALUE %s\",\n                  peer.address, peer.udp_port, peer.node_id.hex()[:8], self.key.hex()[:8])\n        page = self.peer_pages[peer]\n        response = await self.protocol.get_rpc_peer(peer).find_value(self.key, page=page)\n        parsed = FindValueResponse(self.key, response)\n        if not parsed.found:\n            return parsed\n        already_known = len(self.discovered_peers[peer])\n        decoded_peers = set()\n        for compact_addr in parsed.found_compact_addresses:\n            try:\n                decoded_peers.add(decode_tcp_peer_from_compact_address(compact_addr))\n            except ValueError:\n                log.warning(\"misbehaving peer %s:%i returned invalid peer for blob\",\n                            peer.address, peer.udp_port)\n                self.peer_manager.report_failure(peer.address, peer.udp_port)\n                parsed.found_compact_addresses.clear()\n                return parsed\n        self.discovered_peers[peer].update(decoded_peers)\n        log.debug(\"probed %s:%i page %i, %i known\", peer.address, peer.udp_port, page,\n                  already_known + len(parsed.found_compact_addresses))\n        if len(self.discovered_peers[peer]) != already_known + len(parsed.found_compact_addresses):\n            log.warning(\"misbehaving peer %s:%i returned duplicate peers for blob\", peer.address, peer.udp_port)\n        elif len(parsed.found_compact_addresses) >= constants.K and self.peer_pages[peer] < parsed.pages:\n            # the peer returned a full page and indicates it has more\n            self.peer_pages[peer] += 1\n            if peer in self.contacted:\n                # the peer must be removed from self.contacted so that it will be probed for the next page\n                self.contacted.remove(peer)\n        return parsed\n\n    def check_result_ready(self, response: FindValueResponse):\n        if response.found:\n            blob_peers = [decode_tcp_peer_from_compact_address(compact_addr)\n                          for compact_addr in response.found_compact_addresses]\n            to_yield = []\n            for blob_peer in blob_peers:\n                if blob_peer not in self.blob_peers:\n                    self.blob_peers.add(blob_peer)\n                    to_yield.append(blob_peer)\n            if to_yield:\n                self.iteration_queue.put_nowait(to_yield)\n\n    def get_initial_result(self) -> typing.List['KademliaPeer']:\n        if self.protocol.data_store.has_peers_for_blob(self.key):\n            return self.protocol.data_store.get_peers_for_blob(self.key)\n        return []\n"
  },
  {
    "path": "lbry/dht/protocol/protocol.py",
    "content": "import logging\nimport socket\nimport functools\nimport hashlib\nimport asyncio\nimport time\nimport typing\nimport random\nfrom asyncio.protocols import DatagramProtocol\nfrom asyncio.transports import DatagramTransport\n\nfrom prometheus_client import Gauge, Counter, Histogram\n\nfrom lbry.dht import constants\nfrom lbry.dht.serialization.bencoding import DecodeError\nfrom lbry.dht.serialization.datagram import decode_datagram, ErrorDatagram, ResponseDatagram, RequestDatagram\nfrom lbry.dht.serialization.datagram import RESPONSE_TYPE, ERROR_TYPE, PAGE_KEY\nfrom lbry.dht.error import RemoteException, TransportNotConnected\nfrom lbry.dht.protocol.routing_table import TreeRoutingTable\nfrom lbry.dht.protocol.data_store import DictDataStore\nfrom lbry.dht.peer import make_kademlia_peer\n\nif typing.TYPE_CHECKING:\n    from lbry.dht.peer import PeerManager, KademliaPeer\n\nlog = logging.getLogger(__name__)\n\n\nOLD_PROTOCOL_ERRORS = {\n    \"findNode() takes exactly 2 arguments (5 given)\": \"0.19.1\",\n    \"findValue() takes exactly 2 arguments (5 given)\": \"0.19.1\"\n}\n\n\nclass KademliaRPC:\n    stored_blob_metric = Gauge(\n        \"stored_blobs\", \"Number of blobs announced by other peers\", namespace=\"dht_node\",\n        labelnames=(\"scope\",),\n    )\n\n    def __init__(self, protocol: 'KademliaProtocol', loop: asyncio.AbstractEventLoop, peer_port: int = 3333):\n        self.protocol = protocol\n        self.loop = loop\n        self.peer_port = peer_port\n        self.old_token_secret: bytes = None\n        self.token_secret = constants.generate_id()\n\n    def compact_address(self):\n        compact_ip = functools.reduce(lambda buff, x: buff + bytearray([int(x)]),\n                                      self.protocol.external_ip.split('.'), bytearray())\n        compact_port = self.peer_port.to_bytes(2, 'big')\n        return compact_ip + compact_port + self.protocol.node_id\n\n    @staticmethod\n    def ping():\n        return b'pong'\n\n    def store(self, rpc_contact: 'KademliaPeer', blob_hash: bytes, token: bytes, port: int) -> bytes:\n        if len(blob_hash) != constants.HASH_BITS // 8:\n            raise ValueError(f\"invalid length of blob hash: {len(blob_hash)}\")\n        if not 0 < port < 65535:\n            raise ValueError(f\"invalid tcp port: {port}\")\n        rpc_contact.update_tcp_port(port)\n        if not self.verify_token(token, rpc_contact.compact_ip()):\n            if self.loop.time() - self.protocol.started_listening_time < constants.TOKEN_SECRET_REFRESH_INTERVAL:\n                pass\n            else:\n                raise ValueError(\"Invalid token\")\n        self.protocol.data_store.add_peer_to_blob(\n            rpc_contact, blob_hash\n        )\n        self.stored_blob_metric.labels(\"global\").set(len(self.protocol.data_store))\n        return b'OK'\n\n    def find_node(self, rpc_contact: 'KademliaPeer', key: bytes) -> typing.List[typing.Tuple[bytes, str, int]]:\n        if len(key) != constants.HASH_LENGTH:\n            raise ValueError(\"invalid contact node_id length: %i\" % len(key))\n\n        contacts = self.protocol.routing_table.find_close_peers(key, sender_node_id=rpc_contact.node_id)\n        contact_triples = []\n        for contact in contacts[:constants.K * 2]:\n            contact_triples.append((contact.node_id, contact.address, contact.udp_port))\n        return contact_triples\n\n    def find_value(self, rpc_contact: 'KademliaPeer', key: bytes, page: int = 0):\n        page = page if page > 0 else 0\n\n        if len(key) != constants.HASH_LENGTH:\n            raise ValueError(\"invalid blob_exchange hash length: %i\" % len(key))\n\n        response = {\n            b'token': self.make_token(rpc_contact.compact_ip()),\n        }\n\n        if not page:\n            response[b'contacts'] = self.find_node(rpc_contact, key)[:constants.K]\n\n        if self.protocol.protocol_version:\n            response[b'protocolVersion'] = self.protocol.protocol_version\n\n        # get peers we have stored for this blob_exchange\n        peers = [\n            peer.compact_address_tcp()\n            for peer in self.protocol.data_store.get_peers_for_blob(key)\n            if not rpc_contact.tcp_port or peer.compact_address_tcp() != rpc_contact.compact_address_tcp()\n        ]\n        # if we don't have k storing peers to return and we have this hash locally, include our contact information\n        if len(peers) < constants.K and key.hex() in self.protocol.data_store.completed_blobs:\n            peers.append(self.compact_address())\n        if not peers:\n            response[PAGE_KEY] = 0\n        else:\n            response[PAGE_KEY] = (len(peers) // (constants.K + 1)) + 1  # how many pages of peers we have for the blob\n        if len(peers) > constants.K:\n            random.Random(self.protocol.node_id).shuffle(peers)\n        if page * constants.K < len(peers):\n            response[key] = peers[page * constants.K:page * constants.K + constants.K]\n        return response\n\n    def refresh_token(self):  # TODO: this needs to be called periodically\n        self.old_token_secret = self.token_secret\n        self.token_secret = constants.generate_id()\n\n    def make_token(self, compact_ip):\n        h = hashlib.new('sha384')\n        h.update(self.token_secret + compact_ip)\n        return h.digest()\n\n    def verify_token(self, token, compact_ip):\n        h = hashlib.new('sha384')\n        h.update(self.token_secret + compact_ip)\n        if self.old_token_secret and not token == h.digest():  # TODO: why should we be accepting the previous token?\n            h = hashlib.new('sha384')\n            h.update(self.old_token_secret + compact_ip)\n            if not token == h.digest():\n                return False\n        return True\n\n\nclass RemoteKademliaRPC:\n    \"\"\"\n    Encapsulates RPC calls to remote Peers\n    \"\"\"\n\n    def __init__(self, loop: asyncio.AbstractEventLoop, peer_tracker: 'PeerManager', protocol: 'KademliaProtocol',\n                 peer: 'KademliaPeer'):\n        self.loop = loop\n        self.peer_tracker = peer_tracker\n        self.protocol = protocol\n        self.peer = peer\n\n    async def ping(self) -> bytes:\n        \"\"\"\n        :return: b'pong'\n        \"\"\"\n        response = await self.protocol.send_request(\n            self.peer, RequestDatagram.make_ping(self.protocol.node_id)\n        )\n        return response.response\n\n    async def store(self, blob_hash: bytes) -> bytes:\n        \"\"\"\n        :param blob_hash: blob hash as bytes\n        :return: b'OK'\n        \"\"\"\n        if len(blob_hash) != constants.HASH_BITS // 8:\n            raise ValueError(f\"invalid length of blob hash: {len(blob_hash)}\")\n        if not self.protocol.peer_port or not 0 < self.protocol.peer_port < 65535:\n            raise ValueError(f\"invalid tcp port: {self.protocol.peer_port}\")\n        token = self.peer_tracker.get_node_token(self.peer.node_id)\n        if not token:\n            find_value_resp = await self.find_value(blob_hash)\n            token = find_value_resp[b'token']\n        response = await self.protocol.send_request(\n            self.peer, RequestDatagram.make_store(self.protocol.node_id, blob_hash, token, self.protocol.peer_port)\n        )\n        return response.response\n\n    async def find_node(self, key: bytes) -> typing.List[typing.Tuple[bytes, str, int]]:\n        \"\"\"\n        :return: [(node_id, address, udp_port), ...]\n        \"\"\"\n        if len(key) != constants.HASH_BITS // 8:\n            raise ValueError(f\"invalid length of find node key: {len(key)}\")\n        response = await self.protocol.send_request(\n            self.peer, RequestDatagram.make_find_node(self.protocol.node_id, key)\n        )\n        return [(node_id, address.decode(), udp_port) for node_id, address, udp_port in response.response]\n\n    async def find_value(self, key: bytes, page: int = 0) -> typing.Union[typing.Dict]:\n        \"\"\"\n        :return: {\n            b'token': <token bytes>,\n            b'contacts': [(node_id, address, udp_port), ...]\n            <key bytes>: [<blob_peer_compact_address, ...]\n        }\n        \"\"\"\n        if len(key) != constants.HASH_BITS // 8:\n            raise ValueError(f\"invalid length of find value key: {len(key)}\")\n        response = await self.protocol.send_request(\n            self.peer, RequestDatagram.make_find_value(self.protocol.node_id, key, page=page)\n        )\n        self.peer_tracker.update_token(self.peer.node_id, response.response[b'token'])\n        return response.response\n\n\nclass PingQueue:\n    def __init__(self, loop: asyncio.AbstractEventLoop, protocol: 'KademliaProtocol'):\n        self._loop = loop\n        self._protocol = protocol\n        self._pending_contacts: typing.Dict['KademliaPeer', float] = {}\n        self._process_task: asyncio.Task = None\n        self._running = False\n        self._running_pings: typing.Set[asyncio.Task] = set()\n        self._default_delay = constants.MAYBE_PING_DELAY\n\n    @property\n    def running(self):\n        return self._running\n\n    @property\n    def busy(self):\n        return self._running and (any(self._running_pings) or any(self._pending_contacts))\n\n    def enqueue_maybe_ping(self, *peers: 'KademliaPeer', delay: typing.Optional[float] = None):\n        delay = delay if delay is not None else self._default_delay\n        now = self._loop.time()\n        for peer in peers:\n            if peer not in self._pending_contacts or now + delay < self._pending_contacts[peer]:\n                self._pending_contacts[peer] = delay + now\n\n    def maybe_ping(self, peer: 'KademliaPeer'):\n        async def ping_task():\n            try:\n                if self._protocol.peer_manager.peer_is_good(peer):\n                    if not self._protocol.routing_table.get_peer(peer.node_id):\n                        self._protocol.add_peer(peer)\n                    return\n                await self._protocol.get_rpc_peer(peer).ping()\n            except (asyncio.TimeoutError, RemoteException):\n                pass\n\n        task = self._loop.create_task(ping_task())\n        task.add_done_callback(lambda _: None if task not in self._running_pings else self._running_pings.remove(task))\n        self._running_pings.add(task)\n\n    async def _process(self):  # send up to 1 ping per second\n        while True:\n            enqueued = list(self._pending_contacts.keys())\n            now = self._loop.time()\n            for peer in enqueued:\n                if self._pending_contacts[peer] <= now:\n                    del self._pending_contacts[peer]\n                    self.maybe_ping(peer)\n                    break\n            await asyncio.sleep(1)\n\n    def start(self):\n        assert not self._running\n        self._running = True\n        if not self._process_task:\n            self._process_task = self._loop.create_task(self._process())\n\n    def stop(self):\n        assert self._running\n        self._running = False\n        if self._process_task:\n            self._process_task.cancel()\n            self._process_task = None\n        while self._running_pings:\n            self._running_pings.pop().cancel()\n\n\nclass KademliaProtocol(DatagramProtocol):\n    request_sent_metric = Counter(\n        \"request_sent\", \"Number of requests send from DHT RPC protocol\", namespace=\"dht_node\",\n        labelnames=(\"method\",),\n    )\n    request_success_metric = Counter(\n        \"request_success\", \"Number of successful requests\", namespace=\"dht_node\",\n        labelnames=(\"method\",),\n    )\n    request_error_metric = Counter(\n        \"request_error\", \"Number of errors returned from request to other peers\", namespace=\"dht_node\",\n        labelnames=(\"method\",),\n    )\n    HISTOGRAM_BUCKETS = (\n        .005, .01, .025, .05, .075, .1, .25, .5, .75, 1.0, 2.5, 3.0, 3.5, 4.0, 4.50, 5.0, 5.50, 6.0, float('inf')\n    )\n    response_time_metric = Histogram(\n        \"response_time\", \"Response times of DHT RPC requests\", namespace=\"dht_node\", buckets=HISTOGRAM_BUCKETS,\n        labelnames=(\"method\",)\n    )\n    received_request_metric = Counter(\n        \"received_request\", \"Number of received DHT RPC requests\", namespace=\"dht_node\",\n        labelnames=(\"method\",),\n    )\n\n    def __init__(self, loop: asyncio.AbstractEventLoop, peer_manager: 'PeerManager', node_id: bytes, external_ip: str,\n                 udp_port: int, peer_port: int, rpc_timeout: float = constants.RPC_TIMEOUT,\n                 split_buckets_under_index: int = constants.SPLIT_BUCKETS_UNDER_INDEX, is_boostrap_node: bool = False):\n        self.peer_manager = peer_manager\n        self.loop = loop\n        self.node_id = node_id\n        self.external_ip = external_ip\n        self.udp_port = udp_port\n        self.peer_port = peer_port\n        self.is_seed_node = False\n        self.partial_messages: typing.Dict[bytes, typing.Dict[bytes, bytes]] = {}\n        self.sent_messages: typing.Dict[bytes, typing.Tuple['KademliaPeer', asyncio.Future, RequestDatagram]] = {}\n        self.protocol_version = constants.PROTOCOL_VERSION\n        self.started_listening_time = 0\n        self.transport: DatagramTransport = None\n        self.old_token_secret = constants.generate_id()\n        self.token_secret = constants.generate_id()\n        self.routing_table = TreeRoutingTable(\n            self.loop, self.peer_manager, self.node_id, split_buckets_under_index, is_bootstrap_node=is_boostrap_node)\n        self.data_store = DictDataStore(self.loop, self.peer_manager)\n        self.ping_queue = PingQueue(self.loop, self)\n        self.node_rpc = KademliaRPC(self, self.loop, self.peer_port)\n        self.rpc_timeout = rpc_timeout\n        self._split_lock = asyncio.Lock()\n        self._to_remove: typing.Set['KademliaPeer'] = set()\n        self._to_add: typing.Set['KademliaPeer'] = set()\n        self._wakeup_routing_task = asyncio.Event()\n        self.maintaing_routing_task: typing.Optional[asyncio.Task] = None\n\n    @functools.lru_cache(128)\n    def get_rpc_peer(self, peer: 'KademliaPeer') -> RemoteKademliaRPC:\n        return RemoteKademliaRPC(self.loop, self.peer_manager, self, peer)\n\n    def start(self):\n        self.maintaing_routing_task = self.loop.create_task(self.routing_table_task())\n\n    def stop(self):\n        if self.maintaing_routing_task:\n            self.maintaing_routing_task.cancel()\n        if self.transport:\n            self.disconnect()\n\n    def disconnect(self):\n        self.transport.close()\n\n    def connection_made(self, transport: DatagramTransport):\n        self.transport = transport\n\n    def connection_lost(self, exc):\n        self.stop()\n\n    @staticmethod\n    def _migrate_incoming_rpc_args(peer: 'KademliaPeer', method: bytes, *args) -> typing.Tuple[typing.Tuple,\n                                                                                               typing.Dict]:\n        if method == b'store' and peer.protocol_version == 0:\n            if isinstance(args[1], dict):\n                blob_hash = args[0]\n                token = args[1].pop(b'token', None)\n                port = args[1].pop(b'port', -1)\n                original_publisher_id = args[1].pop(b'lbryid', None)\n                age = 0\n                return (blob_hash, token, port, original_publisher_id, age), {}\n        return args, {}\n\n    async def _add_peer(self, peer: 'KademliaPeer'):\n        async def probe(some_peer: 'KademliaPeer'):\n            rpc_peer = self.get_rpc_peer(some_peer)\n            await rpc_peer.ping()\n        return await self.routing_table.add_peer(peer, probe)\n\n    def add_peer(self, peer: 'KademliaPeer'):\n        if peer.node_id == self.node_id:\n            return False\n        self._to_add.add(peer)\n        self._wakeup_routing_task.set()\n\n    def remove_peer(self, peer: 'KademliaPeer'):\n        self._to_remove.add(peer)\n        self._wakeup_routing_task.set()\n\n    async def routing_table_task(self):\n        while True:\n            while self._to_remove:\n                async with self._split_lock:\n                    peer = self._to_remove.pop()\n                    self.routing_table.remove_peer(peer)\n            while self._to_add:\n                async with self._split_lock:\n                    await self._add_peer(self._to_add.pop())\n            await asyncio.gather(self._wakeup_routing_task.wait(), asyncio.sleep(.1))\n            self._wakeup_routing_task.clear()\n\n    def _handle_rpc(self, sender_contact: 'KademliaPeer', message: RequestDatagram):\n        assert sender_contact.node_id != self.node_id, (sender_contact.node_id.hex()[:8],\n                                                        self.node_id.hex()[:8])\n        method = message.method\n        if method not in [b'ping', b'store', b'findNode', b'findValue']:\n            raise AttributeError('Invalid method: %s' % message.method.decode())\n        if message.args and isinstance(message.args[-1], dict) and b'protocolVersion' in message.args[-1]:\n            # args don't need reformatting\n            args, kwargs = tuple(message.args[:-1]), message.args[-1]\n        else:\n            args, kwargs = self._migrate_incoming_rpc_args(sender_contact, message.method, *message.args)\n        log.debug(\"%s:%i RECV CALL %s %s:%i\", self.external_ip, self.udp_port, message.method.decode(),\n                  sender_contact.address, sender_contact.udp_port)\n\n        if method == b'ping':\n            result = self.node_rpc.ping()\n        elif method == b'store':\n            blob_hash, token, port, original_publisher_id, age = args[:5]  # pylint: disable=unused-variable\n            result = self.node_rpc.store(sender_contact, blob_hash, token, port)\n        else:\n            key = args[0]\n            page = kwargs.get(PAGE_KEY, 0)\n            if method == b'findNode':\n                result = self.node_rpc.find_node(sender_contact, key)\n            else:\n                assert method == b'findValue'\n                result = self.node_rpc.find_value(sender_contact, key, page)\n\n        self.send_response(\n            sender_contact, ResponseDatagram(RESPONSE_TYPE, message.rpc_id, self.node_id, result),\n        )\n\n    def handle_request_datagram(self, address: typing.Tuple[str, int], request_datagram: RequestDatagram):\n        # This is an RPC method request\n        self.received_request_metric.labels(method=request_datagram.method).inc()\n        self.peer_manager.report_last_requested(address[0], address[1])\n        peer = self.routing_table.get_peer(request_datagram.node_id)\n        if not peer:\n            try:\n                peer = make_kademlia_peer(request_datagram.node_id, address[0], address[1])\n            except ValueError as err:\n                log.warning(\"error replying to %s: %s\", address[0], str(err))\n                return\n        try:\n            self._handle_rpc(peer, request_datagram)\n            # if the contact is not known to be bad (yet) and we haven't yet queried it, send it a ping so that it\n            # will be added to our routing table if successful\n            is_good = self.peer_manager.peer_is_good(peer)\n            if is_good is None:\n                self.ping_queue.enqueue_maybe_ping(peer)\n            # only add a requesting contact to the routing table if it has replied to one of our requests\n            elif is_good is True:\n                self.add_peer(peer)\n        except ValueError as err:\n            log.debug(\"error raised handling %s request from %s:%i - %s(%s)\",\n                      request_datagram.method, peer.address, peer.udp_port, str(type(err)),\n                      str(err))\n            self.send_error(\n                peer,\n                ErrorDatagram(ERROR_TYPE, request_datagram.rpc_id, self.node_id, str(type(err)).encode(),\n                              str(err).encode())\n            )\n        except Exception as err:\n            log.warning(\"error raised handling %s request from %s:%i - %s(%s)\",\n                        request_datagram.method, peer.address, peer.udp_port, str(type(err)),\n                        str(err))\n            self.send_error(\n                peer,\n                ErrorDatagram(ERROR_TYPE, request_datagram.rpc_id, self.node_id, str(type(err)).encode(),\n                              str(err).encode())\n            )\n\n    def handle_response_datagram(self, address: typing.Tuple[str, int], response_datagram: ResponseDatagram):\n        # Find the message that triggered this response\n        if response_datagram.rpc_id in self.sent_messages:\n            peer, future, _ = self.sent_messages[response_datagram.rpc_id]\n            if peer.address != address[0]:\n                future.set_exception(\n                    RemoteException(f\"response from {address[0]}, expected {peer.address}\")\n                )\n                return\n\n            # We got a result from the RPC\n            if peer.node_id == self.node_id:\n                future.set_exception(RemoteException(\"node has our node id\"))\n                return\n            elif response_datagram.node_id == self.node_id:\n                future.set_exception(RemoteException(\"incoming message is from our node id\"))\n                return\n            peer = make_kademlia_peer(response_datagram.node_id, address[0], address[1])\n            self.peer_manager.report_last_replied(address[0], address[1])\n            self.peer_manager.update_contact_triple(peer.node_id, address[0], address[1])\n            if not future.cancelled():\n                future.set_result(response_datagram)\n                self.add_peer(peer)\n            else:\n                log.warning(\"%s:%i replied, but after we cancelled the request attempt\",\n                            peer.address, peer.udp_port)\n        else:\n            # If the original message isn't found, it must have timed out\n            # TODO: we should probably do something with this...\n            pass\n\n    def handle_error_datagram(self, address, error_datagram: ErrorDatagram):\n        # The RPC request raised a remote exception; raise it locally\n        remote_exception = RemoteException(f\"{error_datagram.exception_type}({error_datagram.response})\")\n        if error_datagram.rpc_id in self.sent_messages:\n            peer, future, request = self.sent_messages.pop(error_datagram.rpc_id)\n            if (peer.address, peer.udp_port) != address:\n                future.set_exception(\n                    RemoteException(\n                        f\"response from {address[0]}:{address[1]}, \"\n                        f\"expected {peer.address}:{peer.udp_port}\"\n                    )\n                )\n                return\n            error_msg = f\"\" \\\n                f\"Error sending '{request.method}' to {peer.address}:{peer.udp_port}\\n\" \\\n                f\"Args: {request.args}\\n\" \\\n                f\"Raised: {str(remote_exception)}\"\n            if 'Invalid token' in error_msg:\n                log.debug(error_msg)\n            elif error_datagram.response not in OLD_PROTOCOL_ERRORS:\n                log.warning(error_msg)\n            else:\n                log.debug(\n                    \"known dht protocol backwards compatibility error with %s:%i (lbrynet v%s)\",\n                    peer.address, peer.udp_port, OLD_PROTOCOL_ERRORS[error_datagram.response]\n                )\n            future.set_exception(remote_exception)\n            return\n        else:\n            if error_datagram.response not in OLD_PROTOCOL_ERRORS:\n                msg = f\"Received error from {address[0]}:{address[1]}, but it isn't in response to a \" \\\n                    f\"pending request: {str(remote_exception)}\"\n                log.warning(msg)\n            else:\n                log.debug(\n                    \"known dht protocol backwards compatibility error with %s:%i (lbrynet v%s)\",\n                    address[0], address[1], OLD_PROTOCOL_ERRORS[error_datagram.response]\n                )\n\n    def datagram_received(self, datagram: bytes, address: typing.Tuple[str, int]) -> None:  # pylint: disable=arguments-renamed\n        try:\n            message = decode_datagram(datagram)\n        except (ValueError, TypeError, DecodeError):\n            self.peer_manager.report_failure(address[0], address[1])\n            log.warning(\"Couldn't decode dht datagram from %s: %s\", address, datagram.hex())\n            return\n\n        if isinstance(message, RequestDatagram):\n            self.handle_request_datagram(address, message)\n        elif isinstance(message, ErrorDatagram):\n            self.handle_error_datagram(address, message)\n        else:\n            assert isinstance(message, ResponseDatagram), \"sanity\"\n            self.handle_response_datagram(address, message)\n\n    async def send_request(self, peer: 'KademliaPeer', request: RequestDatagram) -> ResponseDatagram:\n        self._send(peer, request)\n        response_fut = self.sent_messages[request.rpc_id][1]\n        try:\n            self.request_sent_metric.labels(method=request.method).inc()\n            start = time.perf_counter()\n            response = await asyncio.wait_for(response_fut, self.rpc_timeout)\n            self.response_time_metric.labels(method=request.method).observe(time.perf_counter() - start)\n            self.peer_manager.report_last_replied(peer.address, peer.udp_port)\n            self.request_success_metric.labels(method=request.method).inc()\n            return response\n        except asyncio.CancelledError:\n            if not response_fut.done():\n                response_fut.cancel()\n            raise\n        except (asyncio.TimeoutError, RemoteException):\n            self.request_error_metric.labels(method=request.method).inc()\n            self.peer_manager.report_failure(peer.address, peer.udp_port)\n            if self.peer_manager.peer_is_good(peer) is False:\n                self.remove_peer(peer)\n            raise\n\n    def send_response(self, peer: 'KademliaPeer', response: ResponseDatagram):\n        self._send(peer, response)\n\n    def send_error(self, peer: 'KademliaPeer', error: ErrorDatagram):\n        self._send(peer, error)\n\n    def _send(self, peer: 'KademliaPeer', message: typing.Union[RequestDatagram, ResponseDatagram, ErrorDatagram]):\n        if not self.transport or self.transport.is_closing():\n            raise TransportNotConnected()\n\n        data = message.bencode()\n        if len(data) > constants.MSG_SIZE_LIMIT:\n            log.warning(\"cannot send datagram larger than %i bytes (packet is %i bytes)\",\n                        constants.MSG_SIZE_LIMIT, len(data))\n            log.debug(\"Packet is too large to send: %s\", data[:3500].hex())\n            raise ValueError(\n                f\"cannot send datagram larger than {constants.MSG_SIZE_LIMIT} bytes (packet is {len(data)} bytes)\"\n            )\n        if isinstance(message, (RequestDatagram, ResponseDatagram)):\n            assert message.node_id == self.node_id, message\n            if isinstance(message, RequestDatagram):\n                assert self.node_id != peer.node_id\n\n        def pop_from_sent_messages(_):\n            if message.rpc_id in self.sent_messages:\n                self.sent_messages.pop(message.rpc_id)\n\n        if isinstance(message, RequestDatagram):\n            response_fut = self.loop.create_future()\n            response_fut.add_done_callback(pop_from_sent_messages)\n            self.sent_messages[message.rpc_id] = (peer, response_fut, message)\n        try:\n            self.transport.sendto(data, (peer.address, peer.udp_port))\n        except OSError as err:\n            # TODO: handle ENETUNREACH\n            if err.errno == socket.EWOULDBLOCK:\n                # i'm scared this may swallow important errors, but i get a million of these\n                # on Linux and it doesn't seem to affect anything  -grin\n                log.warning(\"Can't send data to dht: EWOULDBLOCK\")\n            else:\n                log.error(\"DHT socket error sending %i bytes to %s:%i - %s (code %i)\",\n                          len(data), peer.address, peer.udp_port, str(err), err.errno)\n            if isinstance(message, RequestDatagram):\n                self.sent_messages[message.rpc_id][1].set_exception(err)\n            else:\n                raise err\n        if isinstance(message, RequestDatagram):\n            self.peer_manager.report_last_sent(peer.address, peer.udp_port)\n        elif isinstance(message, ErrorDatagram):\n            self.peer_manager.report_failure(peer.address, peer.udp_port)\n\n    def change_token(self):\n        self.old_token_secret = self.token_secret\n        self.token_secret = constants.generate_id()\n\n    def make_token(self, compact_ip):\n        return constants.digest(self.token_secret + compact_ip)\n\n    def verify_token(self, token, compact_ip):\n        h = constants.HASH_CLASS()\n        h.update(self.token_secret + compact_ip)\n        if self.old_token_secret and not token == h.digest():  # TODO: why should we be accepting the previous token?\n            h = constants.HASH_CLASS()\n            h.update(self.old_token_secret + compact_ip)\n            if not token == h.digest():\n                return False\n        return True\n\n    async def store_to_peer(self, hash_value: bytes, peer: 'KademliaPeer',  # pylint: disable=too-many-return-statements\n                            retry: bool = True) -> typing.Tuple[bytes, bool]:\n        async def __store():\n            res = await self.get_rpc_peer(peer).store(hash_value)\n            if res != b\"OK\":\n                raise ValueError(res)\n            log.debug(\"Stored %s to %s\", hash_value.hex()[:8], peer)\n            return peer.node_id, True\n\n        try:\n            return await __store()\n        except asyncio.TimeoutError:\n            log.debug(\"Timeout while storing blob_hash %s at %s\", hash_value.hex()[:8], peer)\n            return peer.node_id, False\n        except ValueError as err:\n            log.error(\"Unexpected response: %s\", err)\n            return peer.node_id, False\n        except RemoteException as err:\n            if 'findValue() takes exactly 2 arguments (5 given)' in str(err):\n                log.debug(\"peer %s:%i is running an incompatible version of lbrynet\", peer.address, peer.udp_port)\n                return peer.node_id, False\n            if 'Invalid token' not in str(err):\n                log.warning(\"Unexpected error while storing blob_hash: %s\", err)\n                return peer.node_id, False\n        self.peer_manager.clear_token(peer.node_id)\n        if not retry:\n            return peer.node_id, False\n        return await self.store_to_peer(hash_value, peer, retry=False)\n"
  },
  {
    "path": "lbry/dht/protocol/routing_table.py",
    "content": "import asyncio\nimport random\nimport logging\nimport typing\nimport itertools\n\nfrom prometheus_client import Gauge\n\nfrom lbry import utils\nfrom lbry.dht import constants\nfrom lbry.dht.error import RemoteException\nfrom lbry.dht.protocol.distance import Distance\nif typing.TYPE_CHECKING:\n    from lbry.dht.peer import KademliaPeer, PeerManager\n\nlog = logging.getLogger(__name__)\n\n\nclass KBucket:\n    \"\"\"\n    Kademlia K-bucket implementation.\n    \"\"\"\n    peer_in_routing_table_metric = Gauge(\n        \"peers_in_routing_table\", \"Number of peers on routing table\", namespace=\"dht_node\",\n        labelnames=(\"scope\",)\n    )\n    peer_with_x_bit_colliding_metric = Gauge(\n        \"peer_x_bit_colliding\", \"Number of peers with at least X bits colliding with this node id\",\n        namespace=\"dht_node\", labelnames=(\"amount\",)\n    )\n\n    def __init__(self, peer_manager: 'PeerManager', range_min: int, range_max: int,\n                 node_id: bytes, capacity: int = constants.K):\n        \"\"\"\n        @param range_min: The lower boundary for the range in the n-bit ID\n                         space covered by this k-bucket\n        @param range_max: The upper boundary for the range in the ID space\n                         covered by this k-bucket\n        \"\"\"\n        self._peer_manager = peer_manager\n        self.range_min = range_min\n        self.range_max = range_max\n        self.peers: typing.List['KademliaPeer'] = []\n        self._node_id = node_id\n        self._distance_to_self = Distance(node_id)\n        self.capacity = capacity\n\n    def add_peer(self, peer: 'KademliaPeer') -> bool:\n        \"\"\" Add contact to _contact list in the right order. This will move the\n        contact to the end of the k-bucket if it is already present.\n\n        @raise kademlia.kbucket.BucketFull: Raised when the bucket is full and\n                                            the contact isn't in the bucket\n                                            already\n\n        @param peer: The contact to add\n        @type peer: dht.contact._Contact\n        \"\"\"\n        if peer in self.peers:\n            # Move the existing contact to the end of the list\n            # - using the new contact to allow add-on data\n            #   (e.g. optimization-specific stuff) to pe updated as well\n            self.peers.remove(peer)\n            self.peers.append(peer)\n            return True\n        else:\n            for i, _ in enumerate(self.peers):\n                local_peer = self.peers[i]\n                if local_peer.node_id == peer.node_id:\n                    self.peers.remove(local_peer)\n                    self.peers.append(peer)\n                    return True\n        if len(self.peers) < self.capacity:\n            self.peers.append(peer)\n            self.peer_in_routing_table_metric.labels(\"global\").inc()\n            bits_colliding = utils.get_colliding_prefix_bits(peer.node_id, self._node_id)\n            self.peer_with_x_bit_colliding_metric.labels(amount=bits_colliding).inc()\n            return True\n        else:\n            return False\n\n    def get_peer(self, node_id: bytes) -> 'KademliaPeer':\n        for peer in self.peers:\n            if peer.node_id == node_id:\n                return peer\n\n    def get_peers(self, count=-1, exclude_contact=None, sort_distance_to=None) -> typing.List['KademliaPeer']:\n        \"\"\" Returns a list containing up to the first count number of contacts\n\n        @param count: The amount of contacts to return (if 0 or less, return\n                      all contacts)\n        @type count: int\n        @param exclude_contact: A node node_id to exclude; if this contact is in\n                               the list of returned values, it will be\n                               discarded before returning. If a C{str} is\n                               passed as this argument, it must be the\n                               contact's ID.\n        @type exclude_contact: str\n\n        @param sort_distance_to: Sort distance to the node_id, defaulting to the parent node node_id. If False don't\n                                 sort the contacts\n\n        @raise IndexError: If the number of requested contacts is too large\n\n        @return: Return up to the first count number of contacts in a list\n                If no contacts are present an empty is returned\n        @rtype: list\n        \"\"\"\n        peers = [peer for peer in self.peers if peer.node_id != exclude_contact]\n\n        # Return all contacts in bucket\n        if count <= 0:\n            count = len(peers)\n\n        # Get current contact number\n        current_len = len(peers)\n\n        # If count greater than k - return only k contacts\n        if count > constants.K:\n            count = constants.K\n\n        if not current_len:\n            return peers\n\n        if sort_distance_to is False:\n            pass\n        else:\n            sort_distance_to = sort_distance_to or self._node_id\n            peers.sort(key=lambda c: Distance(sort_distance_to)(c.node_id))\n\n        return peers[:min(current_len, count)]\n\n    def get_bad_or_unknown_peers(self) -> typing.List['KademliaPeer']:\n        peer = self.get_peers(sort_distance_to=False)\n        return [\n            peer for peer in peer\n            if self._peer_manager.contact_triple_is_good(peer.node_id, peer.address, peer.udp_port) is not True\n        ]\n\n    def remove_peer(self, peer: 'KademliaPeer') -> None:\n        self.peers.remove(peer)\n        self.peer_in_routing_table_metric.labels(\"global\").dec()\n        bits_colliding = utils.get_colliding_prefix_bits(peer.node_id, self._node_id)\n        self.peer_with_x_bit_colliding_metric.labels(amount=bits_colliding).dec()\n\n    def key_in_range(self, key: bytes) -> bool:\n        \"\"\" Tests whether the specified key (i.e. node ID) is in the range\n        of the n-bit ID space covered by this k-bucket (in otherwords, it\n        returns whether or not the specified key should be placed in this\n        k-bucket)\n\n        @param key: The key to test\n        @type key: str or int\n\n        @return: C{True} if the key is in this k-bucket's range, or C{False}\n                 if not.\n        @rtype: bool\n        \"\"\"\n        return self.range_min <= self._distance_to_self(key) < self.range_max\n\n    def __len__(self) -> int:\n        return len(self.peers)\n\n    def __contains__(self, item) -> bool:\n        return item in self.peers\n\n\nclass TreeRoutingTable:\n    \"\"\" This class implements a routing table used by a Node class.\n\n    The Kademlia routing table is a binary tree whose leaves are k-buckets,\n    where each k-bucket contains nodes with some common prefix of their IDs.\n    This prefix is the k-bucket's position in the binary tree; it therefore\n    covers some range of ID values, and together all of the k-buckets cover\n    the entire n-bit ID (or key) space (with no overlap).\n\n    @note: In this implementation, nodes in the tree (the k-buckets) are\n    added dynamically, as needed; this technique is described in the 13-page\n    version of the Kademlia paper, in section 2.4. It does, however, use the\n    ping RPC-based k-bucket eviction algorithm described in section 2.2 of\n    that paper.\n\n    BOOTSTRAP MODE: if set to True, we always add all peers. This is so a\n    bootstrap node does not get a bias towards its own node id and replies are\n    the best it can provide (joining peer knows its neighbors immediately).\n    Over time, this will need to be optimized so we use the disk as holding\n    everything in memory won't be feasible anymore.\n    See: https://github.com/bittorrent/bootstrap-dht\n    \"\"\"\n    bucket_in_routing_table_metric = Gauge(\n        \"buckets_in_routing_table\", \"Number of buckets on routing table\", namespace=\"dht_node\",\n        labelnames=(\"scope\",)\n    )\n\n    def __init__(self, loop: asyncio.AbstractEventLoop, peer_manager: 'PeerManager', parent_node_id: bytes,\n                 split_buckets_under_index: int = constants.SPLIT_BUCKETS_UNDER_INDEX, is_bootstrap_node: bool = False):\n        self._loop = loop\n        self._peer_manager = peer_manager\n        self._parent_node_id = parent_node_id\n        self._split_buckets_under_index = split_buckets_under_index\n        self.buckets: typing.List[KBucket] = [\n            KBucket(\n                self._peer_manager, range_min=0, range_max=2 ** constants.HASH_BITS, node_id=self._parent_node_id,\n                capacity=1 << 32 if is_bootstrap_node else constants.K\n            )\n        ]\n\n    def get_peers(self) -> typing.List['KademliaPeer']:\n        return list(itertools.chain.from_iterable(map(lambda bucket: bucket.peers, self.buckets)))\n\n    def _should_split(self, bucket_index: int, to_add: bytes) -> bool:\n        #  https://stackoverflow.com/questions/32129978/highly-unbalanced-kademlia-routing-table/32187456#32187456\n        if bucket_index < self._split_buckets_under_index:\n            return True\n        contacts = self.get_peers()\n        distance = Distance(self._parent_node_id)\n        contacts.sort(key=lambda c: distance(c.node_id))\n        kth_contact = contacts[-1] if len(contacts) < constants.K else contacts[constants.K - 1]\n        return distance(to_add) < distance(kth_contact.node_id)\n\n    def find_close_peers(self, key: bytes, count: typing.Optional[int] = None,\n                         sender_node_id: typing.Optional[bytes] = None) -> typing.List['KademliaPeer']:\n        exclude = [self._parent_node_id]\n        if sender_node_id:\n            exclude.append(sender_node_id)\n        count = count or constants.K\n        distance = Distance(key)\n        contacts = self.get_peers()\n        contacts = [c for c in contacts if c.node_id not in exclude]\n        if contacts:\n            contacts.sort(key=lambda c: distance(c.node_id))\n            return contacts[:min(count, len(contacts))]\n        return []\n\n    def get_peer(self, contact_id: bytes) -> 'KademliaPeer':\n        return self.buckets[self._kbucket_index(contact_id)].get_peer(contact_id)\n\n    def get_refresh_list(self, start_index: int = 0, force: bool = False) -> typing.List[bytes]:\n        refresh_ids = []\n        for offset, _ in enumerate(self.buckets[start_index:]):\n            refresh_ids.append(self._midpoint_id_in_bucket_range(start_index + offset))\n        # if we have 3 or fewer populated buckets get two random ids in the range of each to try and\n        # populate/split the buckets further\n        buckets_with_contacts = self.buckets_with_contacts()\n        if buckets_with_contacts <= 3:\n            for i in range(buckets_with_contacts):\n                refresh_ids.append(self._random_id_in_bucket_range(i))\n                refresh_ids.append(self._random_id_in_bucket_range(i))\n        return refresh_ids\n\n    def remove_peer(self, peer: 'KademliaPeer') -> None:\n        if not peer.node_id:\n            return\n        bucket_index = self._kbucket_index(peer.node_id)\n        try:\n            self.buckets[bucket_index].remove_peer(peer)\n            self._join_buckets()\n        except ValueError:\n            return\n\n    def _kbucket_index(self, key: bytes) -> int:\n        i = 0\n        for bucket in self.buckets:\n            if bucket.key_in_range(key):\n                return i\n            else:\n                i += 1\n        return i\n\n    def _random_id_in_bucket_range(self, bucket_index: int) -> bytes:\n        random_id = int(random.randrange(self.buckets[bucket_index].range_min, self.buckets[bucket_index].range_max))\n        return Distance(\n            self._parent_node_id\n        )(random_id.to_bytes(constants.HASH_LENGTH, 'big')).to_bytes(constants.HASH_LENGTH, 'big')\n\n    def _midpoint_id_in_bucket_range(self, bucket_index: int) -> bytes:\n        half = int((self.buckets[bucket_index].range_max - self.buckets[bucket_index].range_min) // 2)\n        return Distance(self._parent_node_id)(\n            int(self.buckets[bucket_index].range_min + half).to_bytes(constants.HASH_LENGTH, 'big')\n        ).to_bytes(constants.HASH_LENGTH, 'big')\n\n    def _split_bucket(self, old_bucket_index: int) -> None:\n        \"\"\" Splits the specified k-bucket into two new buckets which together\n        cover the same range in the key/ID space\n\n        @param old_bucket_index: The index of k-bucket to split (in this table's\n                                 list of k-buckets)\n        @type old_bucket_index: int\n        \"\"\"\n        # Resize the range of the current (old) k-bucket\n        old_bucket = self.buckets[old_bucket_index]\n        split_point = old_bucket.range_max - (old_bucket.range_max - old_bucket.range_min) // 2\n        # Create a new k-bucket to cover the range split off from the old bucket\n        new_bucket = KBucket(self._peer_manager, split_point, old_bucket.range_max, self._parent_node_id)\n        old_bucket.range_max = split_point\n        # Now, add the new bucket into the routing table tree\n        self.buckets.insert(old_bucket_index + 1, new_bucket)\n        # Finally, copy all nodes that belong to the new k-bucket into it...\n        for contact in old_bucket.peers:\n            if new_bucket.key_in_range(contact.node_id):\n                new_bucket.add_peer(contact)\n        # ...and remove them from the old bucket\n        for contact in new_bucket.peers:\n            old_bucket.remove_peer(contact)\n        self.bucket_in_routing_table_metric.labels(\"global\").set(len(self.buckets))\n\n    def _join_buckets(self):\n        if len(self.buckets) == 1:\n            return\n        to_pop = [i for i, bucket in enumerate(self.buckets) if len(bucket) == 0]\n        if not to_pop:\n            return\n        log.info(\"join buckets %i\", len(to_pop))\n        bucket_index_to_pop = to_pop[0]\n        assert len(self.buckets[bucket_index_to_pop]) == 0\n        can_go_lower = bucket_index_to_pop - 1 >= 0\n        can_go_higher = bucket_index_to_pop + 1 < len(self.buckets)\n        assert can_go_higher or can_go_lower\n        bucket = self.buckets[bucket_index_to_pop]\n        if can_go_lower and can_go_higher:\n            midpoint = ((bucket.range_max - bucket.range_min) // 2) + bucket.range_min\n            self.buckets[bucket_index_to_pop - 1].range_max = midpoint - 1\n            self.buckets[bucket_index_to_pop + 1].range_min = midpoint\n        elif can_go_lower:\n            self.buckets[bucket_index_to_pop - 1].range_max = bucket.range_max\n        elif can_go_higher:\n            self.buckets[bucket_index_to_pop + 1].range_min = bucket.range_min\n        self.buckets.remove(bucket)\n        self.bucket_in_routing_table_metric.labels(\"global\").set(len(self.buckets))\n        return self._join_buckets()\n\n    def buckets_with_contacts(self) -> int:\n        count = 0\n        for bucket in self.buckets:\n            if len(bucket) > 0:\n                count += 1\n        return count\n\n    async def add_peer(self, peer: 'KademliaPeer', probe: typing.Callable[['KademliaPeer'], typing.Awaitable]):\n        if not peer.node_id:\n            log.warning(\"Tried adding a peer with no node id!\")\n            return False\n        for my_peer in self.get_peers():\n            if (my_peer.address, my_peer.udp_port) == (peer.address, peer.udp_port) and my_peer.node_id != peer.node_id:\n                self.remove_peer(my_peer)\n                self._join_buckets()\n        bucket_index = self._kbucket_index(peer.node_id)\n        if self.buckets[bucket_index].add_peer(peer):\n            return True\n\n        # The bucket is full; see if it can be split (by checking if its range includes the host node's node_id)\n        if self._should_split(bucket_index, peer.node_id):\n            self._split_bucket(bucket_index)\n            # Retry the insertion attempt\n            result = await self.add_peer(peer, probe)\n            self._join_buckets()\n            return result\n        else:\n            # We can't split the k-bucket\n            #\n            # The 13 page kademlia paper specifies that the least recently contacted node in the bucket\n            # shall be pinged. If it fails to reply it is replaced with the new contact. If the ping is successful\n            # the new contact is ignored and not added to the bucket (sections 2.2 and 2.4).\n            #\n            # A reasonable extension to this is BEP 0005, which extends the above:\n            #\n            #    Not all nodes that we learn about are equal. Some are \"good\" and some are not.\n            #    Many nodes using the DHT are able to send queries and receive responses,\n            #    but are not able to respond to queries from other nodes. It is important that\n            #    each node's routing table must contain only known good nodes. A good node is\n            #    a node has responded to one of our queries within the last 15 minutes. A node\n            #    is also good if it has ever responded to one of our queries and has sent us a\n            #    query within the last 15 minutes. After 15 minutes of inactivity, a node becomes\n            #    questionable. Nodes become bad when they fail to respond to multiple queries\n            #    in a row. Nodes that we know are good are given priority over nodes with unknown status.\n            #\n            # When there are bad or questionable nodes in the bucket, the least recent is selected for\n            # potential replacement (BEP 0005). When all nodes in the bucket are fresh, the head (least recent)\n            # contact is selected as described in section 2.2 of the kademlia paper. In both cases the new contact\n            # is ignored if the pinged node replies.\n\n            not_good_contacts = self.buckets[bucket_index].get_bad_or_unknown_peers()\n            not_recently_replied = []\n            for my_peer in not_good_contacts:\n                last_replied = self._peer_manager.get_last_replied(my_peer.address, my_peer.udp_port)\n                if not last_replied or last_replied + 60 < self._loop.time():\n                    not_recently_replied.append(my_peer)\n            if not_recently_replied:\n                to_replace = not_recently_replied[0]\n            else:\n                to_replace = self.buckets[bucket_index].peers[0]\n                last_replied = self._peer_manager.get_last_replied(to_replace.address, to_replace.udp_port)\n                if last_replied and last_replied + 60 > self._loop.time():\n                    return False\n            log.debug(\"pinging %s:%s\", to_replace.address, to_replace.udp_port)\n            try:\n                await probe(to_replace)\n                return False\n            except (asyncio.TimeoutError, RemoteException):\n                log.debug(\"Replacing dead contact in bucket %i: %s:%i with %s:%i \", bucket_index,\n                          to_replace.address, to_replace.udp_port, peer.address, peer.udp_port)\n                if to_replace in self.buckets[bucket_index]:\n                    self.buckets[bucket_index].remove_peer(to_replace)\n                return await self.add_peer(peer, probe)\n"
  },
  {
    "path": "lbry/dht/serialization/__init__.py",
    "content": ""
  },
  {
    "path": "lbry/dht/serialization/bencoding.py",
    "content": "import typing\nfrom lbry.dht.error import DecodeError\n\n\ndef _bencode(data: typing.Union[int, bytes, bytearray, str, list, tuple, dict]) -> bytes:\n    if isinstance(data, int):\n        return b'i%de' % data\n    elif isinstance(data, (bytes, bytearray)):\n        return b'%d:%s' % (len(data), data)\n    elif isinstance(data, str):\n        return b'%d:%s' % (len(data), data.encode())\n    elif isinstance(data, (list, tuple)):\n        encoded_list_items = b''\n        for item in data:\n            encoded_list_items += _bencode(item)\n        return b'l%se' % encoded_list_items\n    elif isinstance(data, dict):\n        encoded_dict_items = b''\n        keys = data.keys()\n        for key in sorted(keys):\n            encoded_dict_items += _bencode(key)\n            encoded_dict_items += _bencode(data[key])\n        return b'd%se' % encoded_dict_items\n    else:\n        raise TypeError(f\"Cannot bencode {type(data)}\")\n\n\ndef _bdecode(data: bytes, start_index: int = 0) -> typing.Tuple[typing.Union[int, bytes, list, tuple, dict], int]:\n    if data[start_index] == ord('i'):\n        end_pos = data[start_index:].find(b'e') + start_index\n        return int(data[start_index + 1:end_pos]), end_pos + 1\n    elif data[start_index] == ord('l'):\n        start_index += 1\n        decoded_list = []\n        while data[start_index] != ord('e'):\n            list_data, start_index = _bdecode(data, start_index)\n            decoded_list.append(list_data)\n        return decoded_list, start_index + 1\n    elif data[start_index] == ord('d'):\n        start_index += 1\n        decoded_dict = {}\n        while data[start_index] != ord('e'):\n            key, start_index = _bdecode(data, start_index)\n            value, start_index = _bdecode(data, start_index)\n            decoded_dict[key] = value\n        return decoded_dict, start_index\n    else:\n        split_pos = data[start_index:].find(b':') + start_index\n        try:\n            length = int(data[start_index:split_pos])\n        except (ValueError, TypeError) as err:\n            raise DecodeError(err)\n        start_index = split_pos + 1\n        end_pos = start_index + length\n        return data[start_index:end_pos], end_pos\n\n\ndef bencode(data: typing.Dict) -> bytes:\n    if not isinstance(data, dict):\n        raise TypeError()\n    return _bencode(data)\n\n\ndef bdecode(data: bytes, allow_non_dict_return: typing.Optional[bool] = False) -> typing.Dict:\n    assert isinstance(data, bytes), DecodeError(f\"invalid data type: {str(type(data))}\")\n\n    if len(data) == 0:\n        raise DecodeError('Cannot decode empty string')\n    try:\n        result = _bdecode(data)[0]\n        if not allow_non_dict_return and not isinstance(result, dict):\n            raise ValueError(f'expected dict, got {type(result)}')\n        return result\n    except (ValueError, TypeError) as err:\n        raise DecodeError(err)\n"
  },
  {
    "path": "lbry/dht/serialization/datagram.py",
    "content": "import typing\nfrom functools import reduce\nfrom lbry.dht import constants\nfrom lbry.dht.serialization.bencoding import bencode, bdecode\n\nREQUEST_TYPE = 0\nRESPONSE_TYPE = 1\nERROR_TYPE = 2\n\nOPTIONAL_ARG_OFFSET = 100\n\n# bencode representation of argument keys\nPAGE_KEY = b'p'\n\nOPTIONAL_FIELDS = ()\n\n\nclass KademliaDatagramBase:\n    \"\"\"\n    field names are used to unwrap/wrap the argument names to index integers that replace them in a datagram\n    all packets have an argument dictionary when bdecoded starting with {0: <int>, 1: <bytes>, 2: <bytes>, ...}\n    these correspond to the packet_type, rpc_id, and node_id args\n    \"\"\"\n\n    required_fields = [\n        'packet_type',\n        'rpc_id',\n        'node_id'\n    ]\n\n    expected_packet_type = -1\n\n    def __init__(self, packet_type: int, rpc_id: bytes, node_id: bytes):\n        self.packet_type = packet_type\n        if self.expected_packet_type != packet_type:\n            raise ValueError(f\"invalid packet type: {packet_type}, expected {self.expected_packet_type}\")\n        if len(rpc_id) != constants.RPC_ID_LENGTH:\n            raise ValueError(f\"invalid rpc node_id: {len(rpc_id)} bytes (expected 20)\")\n        if not len(node_id) == constants.HASH_LENGTH:\n            raise ValueError(f\"invalid node node_id: {len(node_id)} bytes (expected 48)\")\n        self.rpc_id = rpc_id\n        self.node_id = node_id\n\n    def bencode(self) -> bytes:\n        datagram = {\n            i: getattr(self, k) for i, k in enumerate(self.required_fields)\n        }\n        for i, k in enumerate(OPTIONAL_FIELDS):\n            value = getattr(self, k, None)\n            if value is not None:\n                datagram[i + OPTIONAL_ARG_OFFSET] = value\n        return bencode(datagram)\n\n\nclass RequestDatagram(KademliaDatagramBase):\n    required_fields = [\n        'packet_type',\n        'rpc_id',\n        'node_id',\n        'method',\n        'args'\n    ]\n\n    expected_packet_type = REQUEST_TYPE\n\n    def __init__(self, packet_type: int, rpc_id: bytes, node_id: bytes, method: bytes,\n                 args: typing.Optional[typing.List] = None):\n        super().__init__(packet_type, rpc_id, node_id)\n        self.method = method\n        self.args = args or []\n        if not self.args:\n            self.args.append({})\n        if isinstance(self.args[-1], dict):\n            self.args[-1][b'protocolVersion'] = 1\n        else:\n            self.args.append({b'protocolVersion': 1})\n\n    @classmethod\n    def make_ping(cls, from_node_id: bytes, rpc_id: typing.Optional[bytes] = None) -> 'RequestDatagram':\n        rpc_id = rpc_id or constants.generate_id()[:constants.RPC_ID_LENGTH]\n        return cls(REQUEST_TYPE, rpc_id, from_node_id, b'ping')\n\n    @classmethod\n    def make_store(cls, from_node_id: bytes, blob_hash: bytes, token: bytes, port: int,\n                   rpc_id: typing.Optional[bytes] = None) -> 'RequestDatagram':\n        rpc_id = rpc_id or constants.generate_id()[:constants.RPC_ID_LENGTH]\n        if len(blob_hash) != constants.HASH_BITS // 8:\n            raise ValueError(f\"invalid blob hash length: {len(blob_hash)}\")\n        if not 0 < port < 65536:\n            raise ValueError(f\"invalid port: {port}\")\n        if len(token) != constants.HASH_BITS // 8:\n            raise ValueError(f\"invalid token length: {len(token)}\")\n        store_args = [blob_hash, token, port, from_node_id, 0]\n        return cls(REQUEST_TYPE, rpc_id, from_node_id, b'store', store_args)\n\n    @classmethod\n    def make_find_node(cls, from_node_id: bytes, key: bytes,\n                       rpc_id: typing.Optional[bytes] = None) -> 'RequestDatagram':\n        rpc_id = rpc_id or constants.generate_id()[:constants.RPC_ID_LENGTH]\n        if len(key) != constants.HASH_BITS // 8:\n            raise ValueError(f\"invalid key length: {len(key)}\")\n        return cls(REQUEST_TYPE, rpc_id, from_node_id, b'findNode', [key])\n\n    @classmethod\n    def make_find_value(cls, from_node_id: bytes, key: bytes,\n                        rpc_id: typing.Optional[bytes] = None, page: int = 0) -> 'RequestDatagram':\n        rpc_id = rpc_id or constants.generate_id()[:constants.RPC_ID_LENGTH]\n        if len(key) != constants.HASH_BITS // 8:\n            raise ValueError(f\"invalid key length: {len(key)}\")\n        if page < 0:\n            raise ValueError(f\"cannot request a negative page ({page})\")\n        return cls(REQUEST_TYPE, rpc_id, from_node_id, b'findValue', [key, {PAGE_KEY: page}])\n\n\nclass ResponseDatagram(KademliaDatagramBase):\n    required_fields = [\n        'packet_type',\n        'rpc_id',\n        'node_id',\n        'response'\n    ]\n\n    expected_packet_type = RESPONSE_TYPE\n\n    def __init__(self, packet_type: int, rpc_id: bytes, node_id: bytes, response):\n        super().__init__(packet_type, rpc_id, node_id)\n        self.response = response\n\n\nclass ErrorDatagram(KademliaDatagramBase):\n    required_fields = [\n        'packet_type',\n        'rpc_id',\n        'node_id',\n        'exception_type',\n        'response',\n    ]\n\n    expected_packet_type = ERROR_TYPE\n\n    def __init__(self, packet_type: int, rpc_id: bytes, node_id: bytes, exception_type: bytes, response: bytes):\n        super().__init__(packet_type, rpc_id, node_id)\n        self.exception_type = exception_type.decode()\n        self.response = response.decode()\n\n\ndef _decode_datagram(datagram: bytes):\n    msg_types = {\n        REQUEST_TYPE: RequestDatagram,\n        RESPONSE_TYPE: ResponseDatagram,\n        ERROR_TYPE: ErrorDatagram\n    }\n\n    primitive: typing.Dict = bdecode(datagram)\n\n    converted = {\n        str(k).encode() if not isinstance(k, bytes) else k: v for k, v in primitive.items()\n    }\n\n    if converted[b'0'] in [REQUEST_TYPE, ERROR_TYPE, RESPONSE_TYPE]:  # pylint: disable=unsubscriptable-object\n        datagram_type = converted[b'0']  # pylint: disable=unsubscriptable-object\n    else:\n        raise ValueError(\"invalid datagram type\")\n    datagram_class = msg_types[datagram_type]\n    decoded = {\n        k: converted[str(i).encode()]  # pylint: disable=unsubscriptable-object\n        for i, k in enumerate(datagram_class.required_fields)\n        if str(i).encode() in converted  # pylint: disable=unsupported-membership-test\n    }\n    for i, _ in enumerate(OPTIONAL_FIELDS):\n        if str(i + OPTIONAL_ARG_OFFSET).encode() in converted:\n            decoded[i + OPTIONAL_ARG_OFFSET] = converted[str(i + OPTIONAL_ARG_OFFSET).encode()]\n    return decoded, datagram_class\n\n\ndef decode_datagram(datagram: bytes) -> typing.Union[RequestDatagram, ResponseDatagram, ErrorDatagram]:\n    decoded, datagram_class = _decode_datagram(datagram)\n    return datagram_class(**decoded)\n\n\ndef make_compact_ip(address: str) -> bytearray:\n    compact_ip = reduce(lambda buff, x: buff + bytearray([int(x)]), address.split('.'), bytearray())\n    if len(compact_ip) != 4:\n        raise ValueError(\"invalid IPv4 length\")\n    return compact_ip\n\n\ndef make_compact_address(node_id: bytes, address: str, port: int) -> bytearray:\n    compact_ip = make_compact_ip(address)\n    if not 0 < port < 65536:\n        raise ValueError(f'Invalid port: {port}')\n    if len(node_id) != constants.HASH_BITS // 8:\n        raise ValueError(\"invalid node node_id length\")\n    return compact_ip + port.to_bytes(2, 'big') + node_id\n\n\ndef decode_compact_address(compact_address: bytes) -> typing.Tuple[bytes, str, int]:\n    address = \"{}.{}.{}.{}\".format(*compact_address[:4])\n    port = int.from_bytes(compact_address[4:6], 'big')\n    node_id = compact_address[6:]\n    if not 0 < port < 65536:\n        raise ValueError(f'Invalid port: {port}')\n    if len(node_id) != constants.HASH_BITS // 8:\n        raise ValueError(\"invalid node node_id length\")\n    return node_id, address, port\n"
  },
  {
    "path": "lbry/error/Makefile",
    "content": "generate:\n\tpython generate.py generate > __init__.py\n\nanalyze:\n\tpython generate.py analyze\n"
  },
  {
    "path": "lbry/error/README.md",
    "content": "# Exceptions\n\nExceptions in LBRY are defined and generated from the Markdown table at the end of this README.\n\n## Guidelines\n\nWhen 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:\n1. You want to provide a better error message (extend the closest built-in/`aiohttp` exception in this case).\n2. You need to represent a new situation.\n\nWhen defining your own exceptions, consider:\n1. Extending a built-in Python or `aiohttp` exception.\n2. Using contextual variables in the error message.\n\n## Table Column Definitions\n\nColumn | Meaning\n---|---\nCode | 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.\nName | 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).\nMessage | 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.\n\n## Exceptions Table\n\nCode | Name | Message\n---:|---|---\n**1xx** | UserInput | User input errors.\n**10x** | Command | Errors preparing to execute commands.\n101 | CommandDoesNotExist | Command '{command}' does not exist.\n102 | CommandDeprecated | Command '{command}' is deprecated.\n103 | CommandInvalidArgument | Invalid argument '{argument}' to command '{command}'.\n104 | CommandTemporarilyUnavailable | Command '{command}' is temporarily unavailable. -- Such as waiting for required components to start.\n105 | CommandPermanentlyUnavailable | Command '{command}' is permanently unavailable. -- such as when required component was intentionally configured not to start.\n**11x** | InputValue(ValueError) | Invalid argument value provided to command.\n111 | GenericInputValue | The value '{value}' for argument '{argument}' is not valid.\n112 | InputValueIsNone | None or null is not valid value for argument '{argument}'.\n113 | ConflictingInputValue | Only '{first_argument}' or '{second_argument}' is allowed, not both.\n114 | InputStringIsBlank | {argument} cannot be blank.\n115 | EmptyPublishedFile | Cannot publish empty file: {file_path}\n116 | MissingPublishedFile | File does not exist: {file_path}\n117 | InvalidStreamURL | Invalid LBRY stream URL: '{url}' -- When an URL cannot be downloaded, such as '@Channel/' or a collection\n**2xx** | Configuration | Configuration errors.\n201 | ConfigWrite | Cannot write configuration file '{path}'. -- When writing the default config fails on startup, such as due to permission issues.\n202 | ConfigRead | Cannot find provided configuration file '{path}'. -- Can't open the config file user provided via command line args.\n203 | ConfigParse | Failed to parse the configuration file '{path}'. -- Includes the syntax error / line number to help user fix it.\n204 | ConfigMissing | Configuration file '{path}' is missing setting that has no default / fallback.\n205 | ConfigInvalid | Configuration file '{path}' has setting with invalid value.\n**3xx** | Network | **Networking**\n301 | NoInternet | No internet connection.\n302 | NoUPnPSupport | Router does not support UPnP.\n**4xx** | Wallet | **Wallet Errors**\n401 | TransactionRejected | Transaction rejected, unknown reason.\n402 | TransactionFeeTooLow | Fee too low.\n403 | TransactionInvalidSignature | Invalid signature.\n404 | 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.\n405 | ChannelKeyNotFound | Channel signing key not found.\n406 | ChannelKeyInvalid | Channel signing key is out of date. -- For example, channel was updated but you don't have the updated key.\n407 | DataDownload | Failed to download blob. *generic*\n408 | PrivateKeyNotFound | Couldn't find private key for {key} '{value}'.\n410 | Resolve | Failed to resolve '{url}'.\n411 | ResolveTimeout | Failed to resolve '{url}' within the timeout.\n411 | ResolveCensored | Resolve of '{url}' was censored by channel with claim id '{censor_id}'.\n420 | KeyFeeAboveMaxAllowed | {message}\n421 | InvalidPassword | Password is invalid.\n422 | IncompatibleWalletServer | '{server}:{port}' has an incompatibly old version.\n423 | TooManyClaimSearchParameters | {key} cant have more than {limit} items.\n424 | AlreadyPurchased | You already have a purchase for claim_id '{claim_id_hex}'. Use --allow-duplicate-purchase flag to override.\n431 | ServerPaymentInvalidAddress | Invalid address from wallet server: '{address}' - skipping payment round.\n432 | ServerPaymentWalletLocked | Cannot spend funds with locked wallet, skipping payment round.\n433 | ServerPaymentFeeAboveMaxAllowed | Daily server fee of {daily_fee} exceeds maximum configured of {max_fee} LBC.\n434 | WalletNotLoaded | Wallet {wallet_id} is not loaded.\n435 | WalletAlreadyLoaded | Wallet {wallet_path} is already loaded.\n436 | WalletNotFound | Wallet not found at {wallet_path}.\n437 | WalletAlreadyExists | Wallet {wallet_path} already exists, use `wallet_add` to load it.\n**5xx** | Blob | **Blobs**\n500 | BlobNotFound | Blob not found.\n501 | BlobPermissionDenied | Permission denied to read blob.\n502 | BlobTooBig | Blob is too big.\n503 | BlobEmpty | Blob is empty.\n510 | BlobFailedDecryption | Failed to decrypt blob.\n511 | CorruptBlob | Blobs is corrupted.\n520 | BlobFailedEncryption | Failed to encrypt blob.\n531 | DownloadCancelled | Download was canceled.\n532 | DownloadSDTimeout | Failed to download sd blob {download} within timeout.\n533 | DownloadDataTimeout | Failed to download data blobs for sd hash {download} within timeout.\n534 | InvalidStreamDescriptor | {message}\n535 | InvalidData | {message}\n536 | InvalidBlobHash | {message}\n**6xx** | Component | **Components**\n601 | ComponentStartConditionNotMet | Unresolved dependencies for: {components}\n602 | ComponentsNotStarted | {message}\n**7xx** | CurrencyExchange | **Currency Exchange**\n701 | InvalidExchangeRateResponse | Failed to get exchange rate from {source}: {reason}\n702 | CurrencyConversion | {message}\n703 | InvalidCurrency | Invalid currency: {currency} is not a supported currency.\n"
  },
  {
    "path": "lbry/error/__init__.py",
    "content": "from .base import BaseError, claim_id\n\n\nclass UserInputError(BaseError):\n    \"\"\"\n    User input errors.\n    \"\"\"\n\n\nclass CommandError(UserInputError):\n    \"\"\"\n    Errors preparing to execute commands.\n    \"\"\"\n\n\nclass CommandDoesNotExistError(CommandError):\n\n    def __init__(self, command):\n        self.command = command\n        super().__init__(f\"Command '{command}' does not exist.\")\n\n\nclass CommandDeprecatedError(CommandError):\n\n    def __init__(self, command):\n        self.command = command\n        super().__init__(f\"Command '{command}' is deprecated.\")\n\n\nclass CommandInvalidArgumentError(CommandError):\n\n    def __init__(self, argument, command):\n        self.argument = argument\n        self.command = command\n        super().__init__(f\"Invalid argument '{argument}' to command '{command}'.\")\n\n\nclass CommandTemporarilyUnavailableError(CommandError):\n    \"\"\"\n    Such as waiting for required components to start.\n    \"\"\"\n\n    def __init__(self, command):\n        self.command = command\n        super().__init__(f\"Command '{command}' is temporarily unavailable.\")\n\n\nclass CommandPermanentlyUnavailableError(CommandError):\n    \"\"\"\n    such as when required component was intentionally configured not to start.\n    \"\"\"\n\n    def __init__(self, command):\n        self.command = command\n        super().__init__(f\"Command '{command}' is permanently unavailable.\")\n\n\nclass InputValueError(UserInputError, ValueError):\n    \"\"\"\n    Invalid argument value provided to command.\n    \"\"\"\n\n\nclass GenericInputValueError(InputValueError):\n\n    def __init__(self, value, argument):\n        self.value = value\n        self.argument = argument\n        super().__init__(f\"The value '{value}' for argument '{argument}' is not valid.\")\n\n\nclass InputValueIsNoneError(InputValueError):\n\n    def __init__(self, argument):\n        self.argument = argument\n        super().__init__(f\"None or null is not valid value for argument '{argument}'.\")\n\n\nclass ConflictingInputValueError(InputValueError):\n\n    def __init__(self, first_argument, second_argument):\n        self.first_argument = first_argument\n        self.second_argument = second_argument\n        super().__init__(f\"Only '{first_argument}' or '{second_argument}' is allowed, not both.\")\n\n\nclass InputStringIsBlankError(InputValueError):\n\n    def __init__(self, argument):\n        self.argument = argument\n        super().__init__(f\"{argument} cannot be blank.\")\n\n\nclass EmptyPublishedFileError(InputValueError):\n\n    def __init__(self, file_path):\n        self.file_path = file_path\n        super().__init__(f\"Cannot publish empty file: {file_path}\")\n\n\nclass MissingPublishedFileError(InputValueError):\n\n    def __init__(self, file_path):\n        self.file_path = file_path\n        super().__init__(f\"File does not exist: {file_path}\")\n\n\nclass InvalidStreamURLError(InputValueError):\n    \"\"\"\n    When an URL cannot be downloaded, such as '@Channel/' or a collection\n    \"\"\"\n\n    def __init__(self, url):\n        self.url = url\n        super().__init__(f\"Invalid LBRY stream URL: '{url}'\")\n\n\nclass ConfigurationError(BaseError):\n    \"\"\"\n    Configuration errors.\n    \"\"\"\n\n\nclass ConfigWriteError(ConfigurationError):\n    \"\"\"\n    When writing the default config fails on startup, such as due to permission issues.\n    \"\"\"\n\n    def __init__(self, path):\n        self.path = path\n        super().__init__(f\"Cannot write configuration file '{path}'.\")\n\n\nclass ConfigReadError(ConfigurationError):\n    \"\"\"\n    Can't open the config file user provided via command line args.\n    \"\"\"\n\n    def __init__(self, path):\n        self.path = path\n        super().__init__(f\"Cannot find provided configuration file '{path}'.\")\n\n\nclass ConfigParseError(ConfigurationError):\n    \"\"\"\n    Includes the syntax error / line number to help user fix it.\n    \"\"\"\n\n    def __init__(self, path):\n        self.path = path\n        super().__init__(f\"Failed to parse the configuration file '{path}'.\")\n\n\nclass ConfigMissingError(ConfigurationError):\n\n    def __init__(self, path):\n        self.path = path\n        super().__init__(f\"Configuration file '{path}' is missing setting that has no default / fallback.\")\n\n\nclass ConfigInvalidError(ConfigurationError):\n\n    def __init__(self, path):\n        self.path = path\n        super().__init__(f\"Configuration file '{path}' has setting with invalid value.\")\n\n\nclass NetworkError(BaseError):\n    \"\"\"\n    **Networking**\n    \"\"\"\n\n\nclass NoInternetError(NetworkError):\n\n    def __init__(self):\n        super().__init__(\"No internet connection.\")\n\n\nclass NoUPnPSupportError(NetworkError):\n\n    def __init__(self):\n        super().__init__(\"Router does not support UPnP.\")\n\n\nclass WalletError(BaseError):\n    \"\"\"\n    **Wallet Errors**\n    \"\"\"\n\n\nclass TransactionRejectedError(WalletError):\n\n    def __init__(self):\n        super().__init__(\"Transaction rejected, unknown reason.\")\n\n\nclass TransactionFeeTooLowError(WalletError):\n\n    def __init__(self):\n        super().__init__(\"Fee too low.\")\n\n\nclass TransactionInvalidSignatureError(WalletError):\n\n    def __init__(self):\n        super().__init__(\"Invalid signature.\")\n\n\nclass InsufficientFundsError(WalletError):\n    \"\"\"\n    determined by wallet prior to attempting to broadcast a tx; this is different for example from a TX\n    being created and sent but then rejected by lbrycrd for unspendable utxos.\n    \"\"\"\n\n    def __init__(self):\n        super().__init__(\"Not enough funds to cover this transaction.\")\n\n\nclass ChannelKeyNotFoundError(WalletError):\n\n    def __init__(self):\n        super().__init__(\"Channel signing key not found.\")\n\n\nclass ChannelKeyInvalidError(WalletError):\n    \"\"\"\n    For example, channel was updated but you don't have the updated key.\n    \"\"\"\n\n    def __init__(self):\n        super().__init__(\"Channel signing key is out of date.\")\n\n\nclass DataDownloadError(WalletError):\n\n    def __init__(self):\n        super().__init__(\"Failed to download blob. *generic*\")\n\n\nclass PrivateKeyNotFoundError(WalletError):\n\n    def __init__(self, key, value):\n        self.key = key\n        self.value = value\n        super().__init__(f\"Couldn't find private key for {key} '{value}'.\")\n\n\nclass ResolveError(WalletError):\n\n    def __init__(self, url):\n        self.url = url\n        super().__init__(f\"Failed to resolve '{url}'.\")\n\n\nclass ResolveTimeoutError(WalletError):\n\n    def __init__(self, url):\n        self.url = url\n        super().__init__(f\"Failed to resolve '{url}' within the timeout.\")\n\n\nclass ResolveCensoredError(WalletError):\n\n    def __init__(self, url, censor_id, censor_row):\n        self.url = url\n        self.censor_id = censor_id\n        self.censor_row = censor_row\n        super().__init__(f\"Resolve of '{url}' was censored by channel with claim id '{censor_id}'.\")\n\n\nclass KeyFeeAboveMaxAllowedError(WalletError):\n\n    def __init__(self, message):\n        self.message = message\n        super().__init__(f\"{message}\")\n\n\nclass InvalidPasswordError(WalletError):\n\n    def __init__(self):\n        super().__init__(\"Password is invalid.\")\n\n\nclass IncompatibleWalletServerError(WalletError):\n\n    def __init__(self, server, port):\n        self.server = server\n        self.port = port\n        super().__init__(f\"'{server}:{port}' has an incompatibly old version.\")\n\n\nclass TooManyClaimSearchParametersError(WalletError):\n\n    def __init__(self, key, limit):\n        self.key = key\n        self.limit = limit\n        super().__init__(f\"{key} cant have more than {limit} items.\")\n\n\nclass AlreadyPurchasedError(WalletError):\n    \"\"\"\n    allow-duplicate-purchase flag to override.\n    \"\"\"\n\n    def __init__(self, claim_id_hex):\n        self.claim_id_hex = claim_id_hex\n        super().__init__(f\"You already have a purchase for claim_id '{claim_id_hex}'. Use\")\n\n\nclass ServerPaymentInvalidAddressError(WalletError):\n\n    def __init__(self, address):\n        self.address = address\n        super().__init__(f\"Invalid address from wallet server: '{address}' - skipping payment round.\")\n\n\nclass ServerPaymentWalletLockedError(WalletError):\n\n    def __init__(self):\n        super().__init__(\"Cannot spend funds with locked wallet, skipping payment round.\")\n\n\nclass ServerPaymentFeeAboveMaxAllowedError(WalletError):\n\n    def __init__(self, daily_fee, max_fee):\n        self.daily_fee = daily_fee\n        self.max_fee = max_fee\n        super().__init__(f\"Daily server fee of {daily_fee} exceeds maximum configured of {max_fee} LBC.\")\n\n\nclass WalletNotLoadedError(WalletError):\n\n    def __init__(self, wallet_id):\n        self.wallet_id = wallet_id\n        super().__init__(f\"Wallet {wallet_id} is not loaded.\")\n\n\nclass WalletAlreadyLoadedError(WalletError):\n\n    def __init__(self, wallet_path):\n        self.wallet_path = wallet_path\n        super().__init__(f\"Wallet {wallet_path} is already loaded.\")\n\n\nclass WalletNotFoundError(WalletError):\n\n    def __init__(self, wallet_path):\n        self.wallet_path = wallet_path\n        super().__init__(f\"Wallet not found at {wallet_path}.\")\n\n\nclass WalletAlreadyExistsError(WalletError):\n\n    def __init__(self, wallet_path):\n        self.wallet_path = wallet_path\n        super().__init__(f\"Wallet {wallet_path} already exists, use `wallet_add` to load it.\")\n\n\nclass BlobError(BaseError):\n    \"\"\"\n    **Blobs**\n    \"\"\"\n\n\nclass BlobNotFoundError(BlobError):\n\n    def __init__(self):\n        super().__init__(\"Blob not found.\")\n\n\nclass BlobPermissionDeniedError(BlobError):\n\n    def __init__(self):\n        super().__init__(\"Permission denied to read blob.\")\n\n\nclass BlobTooBigError(BlobError):\n\n    def __init__(self):\n        super().__init__(\"Blob is too big.\")\n\n\nclass BlobEmptyError(BlobError):\n\n    def __init__(self):\n        super().__init__(\"Blob is empty.\")\n\n\nclass BlobFailedDecryptionError(BlobError):\n\n    def __init__(self):\n        super().__init__(\"Failed to decrypt blob.\")\n\n\nclass CorruptBlobError(BlobError):\n\n    def __init__(self):\n        super().__init__(\"Blobs is corrupted.\")\n\n\nclass BlobFailedEncryptionError(BlobError):\n\n    def __init__(self):\n        super().__init__(\"Failed to encrypt blob.\")\n\n\nclass DownloadCancelledError(BlobError):\n\n    def __init__(self):\n        super().__init__(\"Download was canceled.\")\n\n\nclass DownloadSDTimeoutError(BlobError):\n\n    def __init__(self, download):\n        self.download = download\n        super().__init__(f\"Failed to download sd blob {download} within timeout.\")\n\n\nclass DownloadDataTimeoutError(BlobError):\n\n    def __init__(self, download):\n        self.download = download\n        super().__init__(f\"Failed to download data blobs for sd hash {download} within timeout.\")\n\n\nclass InvalidStreamDescriptorError(BlobError):\n\n    def __init__(self, message):\n        self.message = message\n        super().__init__(f\"{message}\")\n\n\nclass InvalidDataError(BlobError):\n\n    def __init__(self, message):\n        self.message = message\n        super().__init__(f\"{message}\")\n\n\nclass InvalidBlobHashError(BlobError):\n\n    def __init__(self, message):\n        self.message = message\n        super().__init__(f\"{message}\")\n\n\nclass ComponentError(BaseError):\n    \"\"\"\n    **Components**\n    \"\"\"\n\n\nclass ComponentStartConditionNotMetError(ComponentError):\n\n    def __init__(self, components):\n        self.components = components\n        super().__init__(f\"Unresolved dependencies for: {components}\")\n\n\nclass ComponentsNotStartedError(ComponentError):\n\n    def __init__(self, message):\n        self.message = message\n        super().__init__(f\"{message}\")\n\n\nclass CurrencyExchangeError(BaseError):\n    \"\"\"\n    **Currency Exchange**\n    \"\"\"\n\n\nclass InvalidExchangeRateResponseError(CurrencyExchangeError):\n\n    def __init__(self, source, reason):\n        self.source = source\n        self.reason = reason\n        super().__init__(f\"Failed to get exchange rate from {source}: {reason}\")\n\n\nclass CurrencyConversionError(CurrencyExchangeError):\n\n    def __init__(self, message):\n        self.message = message\n        super().__init__(f\"{message}\")\n\n\nclass InvalidCurrencyError(CurrencyExchangeError):\n\n    def __init__(self, currency):\n        self.currency = currency\n        super().__init__(f\"Invalid currency: {currency} is not a supported currency.\")\n"
  },
  {
    "path": "lbry/error/base.py",
    "content": "from binascii import hexlify\n\n\ndef claim_id(claim_hash):\n    return hexlify(claim_hash[::-1]).decode()\n\n\nclass BaseError(Exception):\n    pass\n"
  },
  {
    "path": "lbry/error/generate.py",
    "content": "import re\nimport sys\nimport argparse\nfrom pathlib import Path\nfrom textwrap import fill, indent\n\n\nINDENT = ' ' * 4\n\nCLASS = \"\"\"\n\nclass {name}({parents}):{doc}\n\"\"\"\n\nINIT = \"\"\"\n    def __init__({args}):{fields}\n        super().__init__({format}\"{message}\")\n\"\"\"\n\nFUNCTIONS = ['claim_id']\n\n\nclass ErrorClass:\n\n    def __init__(self, hierarchy, name, message):\n        self.hierarchy = hierarchy.replace('**', '')\n        self.other_parents = []\n        if '(' in name:\n            assert ')' in name, f\"Missing closing parenthesis in '{name}'.\"\n            self.other_parents = name[name.find('(')+1:name.find(')')].split(',')\n            name = name[:name.find('(')]\n        self.name = name\n        self.class_name = name+'Error'\n        self.message = message\n        self.comment = \"\"\n        if '--' in message:\n            self.message, self.comment = message.split('--')\n        self.message = self.message.strip()\n        self.comment = self.comment.strip()\n\n    @property\n    def is_leaf(self):\n        return 'x' not in self.hierarchy\n\n    @property\n    def code(self):\n        return self.hierarchy.replace('x', '')\n\n    @property\n    def parent_codes(self):\n        return self.hierarchy[0:2], self.hierarchy[0]\n\n    def get_arguments(self):\n        args = ['self']\n        for arg in re.findall('{([a-z0-1_()]+)}', self.message):\n            for func in FUNCTIONS:\n                if arg.startswith(f'{func}('):\n                    arg = arg[len(f'{func}('):-1]\n                    break\n            args.append(arg)\n        return args\n\n    @staticmethod\n    def get_fields(args):\n        if len(args) > 1:\n            return ''.join(f'\\n{INDENT*2}self.{field} = {field}' for field in args[1:])\n        return ''\n\n    @staticmethod\n    def get_doc_string(doc):\n        if doc:\n            return f'\\n{INDENT}\"\"\"\\n{indent(fill(doc, 100), INDENT)}\\n{INDENT}\"\"\"'\n        return \"\"\n\n    def render(self, out, parent):\n        if not parent:\n            parents = ['BaseError']\n        else:\n            parents = [parent.class_name]\n        parents += self.other_parents\n        args = self.get_arguments()\n        if self.is_leaf:\n            out.write((CLASS + INIT).format(\n                name=self.class_name, parents=', '.join(parents),\n                args=', '.join(args), fields=self.get_fields(args),\n                message=self.message, doc=self.get_doc_string(self.comment), format='f' if len(args) > 1 else ''\n            ))\n        else:\n            out.write(CLASS.format(\n                name=self.class_name, parents=', '.join(parents),\n                doc=self.get_doc_string(self.comment or self.message)\n            ))\n\n\ndef get_errors():\n    with open('README.md', 'r') as readme:\n        lines = iter(readme.readlines())\n        for line in lines:\n            if line.startswith('## Exceptions Table'):\n                break\n        for line in lines:\n            if line.startswith('---:|'):\n                break\n        for line in lines:\n            if not line:\n                break\n            yield ErrorClass(*[c.strip() for c in line.split('|')])\n\n\ndef find_parent(stack, child):\n    for parent_code in child.parent_codes:\n        parent = stack.get(parent_code)\n        if parent:\n            return parent\n\n\ndef generate(out):\n    out.write(f\"from .base import BaseError, {', '.join(FUNCTIONS)}\\n\")\n    stack = {}\n    for error in get_errors():\n        error.render(out, find_parent(stack, error))\n        if not error.is_leaf:\n            assert error.code not in stack, f\"Duplicate code: {error.code}\"\n            stack[error.code] = error\n\n\ndef analyze():\n    errors = {e.class_name: [] for e in get_errors() if e.is_leaf}\n    here = Path(__file__).absolute().parents[0]\n    module = here.parent\n    for file_path in module.glob('**/*.py'):\n        if here in file_path.parents:\n            continue\n        with open(file_path) as src_file:\n            src = src_file.read()\n            for error in errors.keys():\n                found = src.count(error)\n                if found > 0:\n                    errors[error].append((file_path, found))\n\n    print('Unused Errors:\\n')\n    for error, used in errors.items():\n        if used:\n            print(f' - {error}')\n            for use in used:\n                print(f'   {use[0].relative_to(module.parent)} {use[1]}')\n            print('')\n\n    print('')\n    print('Unused Errors:')\n    for error, used in errors.items():\n        if not used:\n            print(f' - {error}')\n\n\ndef main():\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"action\", choices=['generate', 'analyze'])\n    args = parser.parse_args()\n    if args.action == \"analyze\":\n        analyze()\n    elif args.action == \"generate\":\n        generate(sys.stdout)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "lbry/extras/__init__.py",
    "content": ""
  },
  {
    "path": "lbry/extras/cli.py",
    "content": "import os\nimport sys\nimport shutil\nimport signal\nimport pathlib\nimport json\nimport asyncio\nimport argparse\nimport logging\nimport logging.handlers\n\nimport aiohttp\nfrom aiohttp.web import GracefulExit\nfrom docopt import docopt\n\nfrom lbry import __version__ as lbrynet_version\nfrom lbry.extras.daemon.daemon import Daemon\nfrom lbry.conf import Config, CLIConfig\n\nlog = logging.getLogger('lbry')\n\n\ndef display(data):\n    print(json.dumps(data, indent=2))\n\n\nasync def execute_command(conf, method, params, callback=display):\n    async with aiohttp.ClientSession() as session:\n        try:\n            message = {'method': method, 'params': params}\n            async with session.get(conf.api_connection_url, json=message) as resp:\n                try:\n                    data = await resp.json()\n                    if 'result' in data:\n                        return callback(data['result'])\n                    elif 'error' in data:\n                        return callback(data['error'])\n                except Exception as e:\n                    log.exception('Could not process response from server:', exc_info=e)\n        except aiohttp.ClientConnectionError:\n            print(\"Could not connect to daemon. Are you sure it's running?\")\n\n\ndef normalize_value(x, key=None):\n    if not isinstance(x, str):\n        return x\n    if key in ('uri', 'channel_name', 'name', 'file_name', 'claim_name', 'download_directory'):\n        return x\n    if x.lower() == 'true':\n        return True\n    if x.lower() == 'false':\n        return False\n    if x.isdigit():\n        return int(x)\n    return x\n\n\ndef remove_brackets(key):\n    if key.startswith(\"<\") and key.endswith(\">\"):\n        return str(key[1:-1])\n    return key\n\n\ndef set_kwargs(parsed_args):\n    kwargs = {}\n    for key, arg in parsed_args.items():\n        if arg is None:\n            continue\n        k = None\n        if key.startswith(\"--\") and remove_brackets(key[2:]) not in kwargs:\n            k = remove_brackets(key[2:])\n        elif remove_brackets(key) not in kwargs:\n            k = remove_brackets(key)\n        kwargs[k] = normalize_value(arg, k)\n    return kwargs\n\n\ndef split_subparser_argument(parent, original, name, condition):\n    new_sub_parser = argparse._SubParsersAction(\n        original.option_strings,\n        original._prog_prefix,\n        original._parser_class,\n        metavar=original.metavar\n    )\n    new_sub_parser._name_parser_map = original._name_parser_map\n    new_sub_parser._choices_actions = [\n        a for a in original._choices_actions if condition(original._name_parser_map[a.dest])\n    ]\n    group = argparse._ArgumentGroup(parent, name)\n    group._group_actions = [new_sub_parser]\n    return group\n\n\nclass ArgumentParser(argparse.ArgumentParser):\n    def __init__(self, *args, group_name=None, **kwargs):\n        super().__init__(*args, formatter_class=HelpFormatter, add_help=False, **kwargs)\n        self.add_argument(\n            '--help', dest='help', action='store_true', default=False,\n            help='Show this help message and exit.'\n        )\n        self._optionals.title = 'Options'\n        if group_name is None:\n            self.epilog = (\n                \"Run 'lbrynet COMMAND --help' for more information on a command or group.\"\n            )\n        else:\n            self.epilog = (\n                f\"Run 'lbrynet {group_name} COMMAND --help' for more information on a command.\"\n            )\n            self.set_defaults(group=group_name, group_parser=self)\n\n    def format_help(self):\n        formatter = self._get_formatter()\n        formatter.add_usage(\n            self.usage, self._actions, self._mutually_exclusive_groups\n        )\n        formatter.add_text(self.description)\n\n        # positionals, optionals and user-defined groups\n        for action_group in self._granular_action_groups:\n            formatter.start_section(action_group.title)\n            formatter.add_text(action_group.description)\n            formatter.add_arguments(action_group._group_actions)\n            formatter.end_section()\n\n        formatter.add_text(self.epilog)\n        return formatter.format_help()\n\n    @property\n    def _granular_action_groups(self):\n        if self.prog != 'lbrynet':\n            yield from self._action_groups\n            return\n        yield self._optionals\n        action: argparse._SubParsersAction = self._positionals._group_actions[0]\n        yield split_subparser_argument(\n            self, action, \"Grouped Commands\", lambda parser: 'group' in parser._defaults\n        )\n        yield split_subparser_argument(\n            self, action, \"Commands\", lambda parser: 'group' not in parser._defaults\n        )\n\n    def error(self, message):\n        self.print_help(argparse._sys.stderr)\n        self.exit(2, f\"\\n{message}\\n\")\n\n\nclass HelpFormatter(argparse.HelpFormatter):\n\n    def add_usage(self, usage, actions, groups, prefix='Usage:  '):\n        super().add_usage(\n            usage, [a for a in actions if a.option_strings != ['--help']], groups, prefix\n        )\n\n\ndef add_command_parser(parent, command):\n    subcommand = parent.add_parser(\n        command['name'],\n        help=command['doc'].strip().splitlines()[0]\n    )\n    subcommand.set_defaults(\n        api_method_name=command['api_method_name'],\n        command=command['name'],\n        doc=command['doc'],\n        replaced_by=command.get('replaced_by', None)\n    )\n\n\ndef get_argument_parser():\n    root = ArgumentParser(\n        'lbrynet', description='An interface to the LBRY Network.', allow_abbrev=False,\n    )\n    root.add_argument(\n        '-v', '--version', dest='cli_version', action=\"store_true\",\n        help='Show lbrynet CLI version and exit.'\n    )\n    root.set_defaults(group=None, command=None)\n    CLIConfig.contribute_to_argparse(root)\n    sub = root.add_subparsers(metavar='COMMAND')\n    start = sub.add_parser(\n        'start',\n        usage='lbrynet start [--config FILE] [--data-dir DIR] [--wallet-dir DIR] [--download-dir DIR] ...',\n        help='Start LBRY Network interface.'\n    )\n    start.add_argument(\n        '--quiet', dest='quiet', action=\"store_true\",\n        help='Disable all console output.'\n    )\n    start.add_argument(\n        '--no-logging', dest='no_logging', action=\"store_true\",\n        help='Disable all logging of any kind.'\n    )\n    start.add_argument(\n        '--verbose', nargs=\"*\",\n        help=('Enable debug output for lbry logger and event loop. Optionally specify loggers for which debug output '\n              'should selectively be applied.')\n    )\n    start.add_argument(\n        '--initial-headers', dest='initial_headers',\n        help='Specify path to initial blockchain headers, faster than downloading them on first run.'\n    )\n    Config.contribute_to_argparse(start)\n    start.set_defaults(command='start', start_parser=start, doc=start.format_help())\n\n    api = Daemon.get_api_definitions()\n    groups = {}\n    for group_name in sorted(api['groups']):\n        group_parser = sub.add_parser(group_name, group_name=group_name, help=api['groups'][group_name])\n        groups[group_name] = group_parser.add_subparsers(metavar='COMMAND')\n\n    nicer_order = ['stop', 'get', 'publish', 'resolve']\n    for command_name in sorted(api['commands']):\n        if command_name not in nicer_order:\n            nicer_order.append(command_name)\n\n    for command_name in nicer_order:\n        command = api['commands'][command_name]\n        if command['group'] is None:\n            add_command_parser(sub, command)\n        else:\n            add_command_parser(groups[command['group']], command)\n\n    return root\n\n\ndef ensure_directory_exists(path: str):\n    if not os.path.isdir(path):\n        pathlib.Path(path).mkdir(parents=True, exist_ok=True)\n    use_effective_ids = os.access in os.supports_effective_ids\n    if not os.access(path, os.W_OK, effective_ids=use_effective_ids):\n        raise PermissionError(f\"The following directory is not writable: {path}\")\n\n\nLOG_MODULES = 'lbry', 'aioupnp'\n\n\ndef setup_logging(logger: logging.Logger, args: argparse.Namespace, conf: Config):\n    default_formatter = logging.Formatter(\"%(asctime)s %(levelname)-8s %(name)s:%(lineno)d: %(message)s\")\n    file_handler = logging.handlers.RotatingFileHandler(conf.log_file_path, maxBytes=2097152, backupCount=5)\n    file_handler.setFormatter(default_formatter)\n    for module_name in LOG_MODULES:\n        logger.getChild(module_name).addHandler(file_handler)\n    if not args.quiet:\n        handler = logging.StreamHandler()\n        handler.setFormatter(default_formatter)\n        for module_name in LOG_MODULES:\n            logger.getChild(module_name).addHandler(handler)\n\n    logger.getChild('lbry').setLevel(logging.INFO)\n    logger.getChild('aioupnp').setLevel(logging.WARNING)\n    logger.getChild('aiohttp').setLevel(logging.CRITICAL)\n\n    if args.verbose is not None:\n        if len(args.verbose) > 0:\n            for module in args.verbose:\n                logger.getChild(module).setLevel(logging.DEBUG)\n        else:\n            logger.getChild('lbry').setLevel(logging.DEBUG)\n\n\ndef run_daemon(args: argparse.Namespace, conf: Config):\n    loop = asyncio.get_event_loop()\n    if args.verbose is not None:\n        loop.set_debug(True)\n    if not args.no_logging:\n        setup_logging(logging.getLogger(), args, conf)\n    daemon = Daemon(conf)\n\n    def __exit():\n        raise GracefulExit()\n\n    try:\n        loop.add_signal_handler(signal.SIGINT, __exit)\n        loop.add_signal_handler(signal.SIGTERM, __exit)\n    except NotImplementedError:\n        pass  # Not implemented on Windows\n\n    try:\n        loop.run_until_complete(daemon.start())\n        loop.run_forever()\n    except (GracefulExit, KeyboardInterrupt, asyncio.CancelledError):\n        pass\n    finally:\n        loop.run_until_complete(daemon.stop())\n        logging.shutdown()\n\n    if hasattr(loop, 'shutdown_asyncgens'):\n        loop.run_until_complete(loop.shutdown_asyncgens())\n\n\ndef main(argv=None):\n    argv = argv or sys.argv[1:]\n    parser = get_argument_parser()\n    args, command_args = parser.parse_known_args(argv)\n\n    conf = Config.create_from_arguments(args)\n    for directory in (conf.data_dir, conf.download_dir, conf.wallet_dir):\n        ensure_directory_exists(directory)\n\n    if args.cli_version:\n        print(f\"lbrynet {lbrynet_version}\")\n    elif args.command == 'start':\n        if args.help:\n            args.start_parser.print_help()\n        else:\n            if args.initial_headers:\n                ledger_path = os.path.join(conf.wallet_dir, 'lbc_mainnet')\n                ensure_directory_exists(ledger_path)\n                current_size = 0\n                headers_path = os.path.join(ledger_path, 'headers')\n                if os.path.exists(headers_path):\n                    current_size = os.stat(headers_path).st_size\n                if os.stat(args.initial_headers).st_size > current_size:\n                    log.info('Copying header from %s to %s', args.initial_headers, headers_path)\n                    shutil.copy(args.initial_headers, headers_path)\n            run_daemon(args, conf)\n    elif args.command is not None:\n        doc = args.doc\n        api_method_name = args.api_method_name\n        if args.replaced_by:\n            print(f\"{args.api_method_name} is deprecated, using {args.replaced_by['api_method_name']}.\")\n            doc = args.replaced_by['doc']\n            api_method_name = args.replaced_by['api_method_name']\n        if args.help:\n            print(doc)\n        else:\n            parsed = docopt(doc, command_args)\n            params = set_kwargs(parsed)\n            asyncio.get_event_loop().run_until_complete(execute_command(conf, api_method_name, params))\n    elif args.group is not None:\n        args.group_parser.print_help()\n    else:\n        parser.print_help()\n\n    return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": "lbry/extras/daemon/__init__.py",
    "content": ""
  },
  {
    "path": "lbry/extras/daemon/analytics.py",
    "content": "import asyncio\nimport collections\nimport logging\nimport typing\nimport aiohttp\nfrom lbry import utils\nfrom lbry.conf import Config\nfrom lbry.extras import system_info\n\nANALYTICS_ENDPOINT = 'https://api.segment.io/v1'\nANALYTICS_TOKEN = 'Ax5LZzR1o3q3Z3WjATASDwR5rKyHH0qOIRIbLmMXn2H='\n\n# Things We Track\nSERVER_STARTUP = 'Server Startup'\nSERVER_STARTUP_SUCCESS = 'Server Startup Success'\nSERVER_STARTUP_ERROR = 'Server Startup Error'\nDOWNLOAD_STARTED = 'Download Started'\nDOWNLOAD_ERRORED = 'Download Errored'\nDOWNLOAD_FINISHED = 'Download Finished'\nHEARTBEAT = 'Heartbeat'\nDISK_SPACE = 'Disk Space'\nCLAIM_ACTION = 'Claim Action'  # publish/create/update/abandon\nNEW_CHANNEL = 'New Channel'\nCREDITS_SENT = 'Credits Sent'\nUPNP_SETUP = \"UPnP Setup\"\n\nBLOB_BYTES_UPLOADED = 'Blob Bytes Uploaded'\n\n\nTIME_TO_FIRST_BYTES = \"Time To First Bytes\"\n\n\nlog = logging.getLogger(__name__)\n\n\ndef _event_properties(installation_id: str, session_id: str,\n                      event_properties: typing.Optional[typing.Dict]) -> typing.Dict:\n    properties = {\n        'lbry_id': installation_id,\n        'session_id': session_id,\n    }\n    properties.update(event_properties or {})\n    return properties\n\n\ndef _download_properties(conf: Config, external_ip: str, resolve_duration: float,\n                         total_duration: typing.Optional[float], download_id: str, name: str,\n                         outpoint: str, active_peer_count: typing.Optional[int],\n                         tried_peers_count: typing.Optional[int], connection_failures_count: typing.Optional[int],\n                         added_fixed_peers: bool, fixed_peer_delay: float, sd_hash: str,\n                         sd_download_duration: typing.Optional[float] = None,\n                         head_blob_hash: typing.Optional[str] = None,\n                         head_blob_length: typing.Optional[int] = None,\n                         head_blob_download_duration: typing.Optional[float] = None,\n                         error: typing.Optional[str] = None, error_msg: typing.Optional[str] = None,\n                         wallet_server: typing.Optional[str] = None) -> typing.Dict:\n    return {\n        \"external_ip\": external_ip,\n        \"download_id\": download_id,\n        \"total_duration\": round(total_duration, 4),\n        \"resolve_duration\": None if not resolve_duration else round(resolve_duration, 4),\n        \"error\": error,\n        \"error_message\": error_msg,\n        'name': name,\n        \"outpoint\": outpoint,\n\n        \"node_rpc_timeout\": conf.node_rpc_timeout,\n        \"peer_connect_timeout\": conf.peer_connect_timeout,\n        \"blob_download_timeout\": conf.blob_download_timeout,\n        \"use_fixed_peers\": len(conf.fixed_peers) > 0,\n        \"fixed_peer_delay\": fixed_peer_delay,\n        \"added_fixed_peers\": added_fixed_peers,\n        \"active_peer_count\": active_peer_count,\n        \"tried_peers_count\": tried_peers_count,\n\n        \"sd_blob_hash\": sd_hash,\n        \"sd_blob_duration\": None if not sd_download_duration else round(sd_download_duration, 4),\n\n        \"head_blob_hash\": head_blob_hash,\n        \"head_blob_length\": head_blob_length,\n        \"head_blob_duration\": None if not head_blob_download_duration else round(head_blob_download_duration, 4),\n\n        \"connection_failures_count\": connection_failures_count,\n        \"wallet_server\": wallet_server\n    }\n\n\ndef _make_context(platform):\n    # see https://segment.com/docs/spec/common/#context\n    # they say they'll ignore fields outside the spec, but evidently they don't\n    context = {\n        'app': {\n            'version': platform['lbrynet_version'],\n            'build': platform['build'],\n        },\n        # TODO: expand os info to give linux/osx specific info\n        'os': {\n            'name': platform['os_system'],\n            'version': platform['os_release']\n        },\n    }\n    if 'desktop' in platform and 'distro' in platform:\n        context['os']['desktop'] = platform['desktop']\n        context['os']['distro'] = platform['distro']\n    return context\n\n\nclass AnalyticsManager:\n    def __init__(self, conf: Config, installation_id: str, session_id: str):\n        self.conf = conf\n        self.cookies = {}\n        self.url = ANALYTICS_ENDPOINT\n        self._write_key = utils.deobfuscate(ANALYTICS_TOKEN)\n        self._tracked_data = collections.defaultdict(list)\n        self.context = _make_context(system_info.get_platform())\n        self.installation_id = installation_id\n        self.session_id = session_id\n        self.task: typing.Optional[asyncio.Task] = None\n        self.external_ip: typing.Optional[str] = None\n\n    @property\n    def enabled(self):\n        return self.conf.share_usage_data\n\n    @property\n    def is_started(self):\n        return self.task is not None\n\n    async def start(self):\n        if self.task is None:\n            self.task = asyncio.create_task(self.run())\n\n    async def run(self):\n        while True:\n            if self.enabled:\n                self.external_ip, _ = await utils.get_external_ip(self.conf.lbryum_servers)\n                await self._send_heartbeat()\n            await asyncio.sleep(1800)\n\n    def stop(self):\n        if self.task is not None and not self.task.done():\n            self.task.cancel()\n\n    async def _post(self, data: typing.Dict):\n        request_kwargs = {\n            'method': 'POST',\n            'url': self.url + '/track',\n            'headers': {'Connection': 'Close'},\n            'auth': aiohttp.BasicAuth(self._write_key, ''),\n            'json': data,\n            'cookies': self.cookies\n        }\n        try:\n            async with utils.aiohttp_request(**request_kwargs) as response:\n                self.cookies.update(response.cookies)\n        except Exception as e:\n            log.debug('Encountered an exception while POSTing to %s: ', self.url + '/track', exc_info=e)\n\n    async def track(self, event: typing.Dict):\n        \"\"\"Send a single tracking event\"\"\"\n        if self.enabled:\n            log.debug('Sending track event: %s', event)\n            await self._post(event)\n\n    async def send_upnp_setup_success_fail(self, success, status):\n        await self.track(\n            self._event(UPNP_SETUP, {\n                'success': success,\n                'status': status,\n            })\n        )\n\n    async def send_disk_space_used(self, storage_used, storage_limit, is_from_network_quota):\n        await self.track(\n            self._event(DISK_SPACE, {\n                'used': storage_used,\n                'limit': storage_limit,\n                'from_network_quota': is_from_network_quota\n            })\n        )\n\n    async def send_server_startup(self):\n        await self.track(self._event(SERVER_STARTUP))\n\n    async def send_server_startup_success(self):\n        await self.track(self._event(SERVER_STARTUP_SUCCESS))\n\n    async def send_server_startup_error(self, message):\n        await self.track(self._event(SERVER_STARTUP_ERROR, {'message': message}))\n\n    async def send_time_to_first_bytes(self, resolve_duration: typing.Optional[float],\n                                       total_duration: typing.Optional[float], download_id: str,\n                                       name: str, outpoint: typing.Optional[str],\n                                       found_peers_count: typing.Optional[int],\n                                       tried_peers_count: typing.Optional[int],\n                                       connection_failures_count: typing.Optional[int],\n                                       added_fixed_peers: bool,\n                                       fixed_peers_delay: float, sd_hash: str,\n                                       sd_download_duration: typing.Optional[float] = None,\n                                       head_blob_hash: typing.Optional[str] = None,\n                                       head_blob_length: typing.Optional[int] = None,\n                                       head_blob_duration: typing.Optional[int] = None,\n                                       error: typing.Optional[str] = None,\n                                       error_msg: typing.Optional[str] = None,\n                                       wallet_server: typing.Optional[str] = None):\n        await self.track(self._event(TIME_TO_FIRST_BYTES, _download_properties(\n            self.conf, self.external_ip, resolve_duration, total_duration, download_id, name, outpoint,\n            found_peers_count, tried_peers_count, connection_failures_count, added_fixed_peers, fixed_peers_delay,\n            sd_hash, sd_download_duration, head_blob_hash, head_blob_length, head_blob_duration, error, error_msg,\n            wallet_server\n        )))\n\n    async def send_download_finished(self, download_id, name, sd_hash):\n        await self.track(\n            self._event(\n                DOWNLOAD_FINISHED, {\n                    'download_id': download_id,\n                    'name': name,\n                    'stream_info': sd_hash\n                }\n            )\n        )\n\n    async def send_claim_action(self, action):\n        await self.track(self._event(CLAIM_ACTION, {'action': action}))\n\n    async def send_new_channel(self):\n        await self.track(self._event(NEW_CHANNEL))\n\n    async def send_credits_sent(self):\n        await self.track(self._event(CREDITS_SENT))\n\n    async def _send_heartbeat(self):\n        await self.track(self._event(HEARTBEAT))\n\n    def _event(self, event, properties: typing.Optional[typing.Dict] = None):\n        return {\n            'userId': 'lbry',\n            'event': event,\n            'properties': _event_properties(self.installation_id, self.session_id, properties),\n            'context': self.context,\n            'timestamp': utils.isonow()\n        }\n"
  },
  {
    "path": "lbry/extras/daemon/client.py",
    "content": "from lbry.extras.cli import execute_command\nfrom lbry.conf import Config\n\n\ndef daemon_rpc(conf: Config, method: str, **kwargs):\n    return execute_command(conf, method, kwargs, callback=lambda data: data)\n"
  },
  {
    "path": "lbry/extras/daemon/component.py",
    "content": "import asyncio\nimport logging\nfrom lbry.conf import Config\nfrom lbry.extras.daemon.componentmanager import ComponentManager\n\nlog = logging.getLogger(__name__)\n\n\nclass ComponentType(type):\n    def __new__(mcs, name, bases, newattrs):\n        klass = type.__new__(mcs, name, bases, newattrs)\n        if name != \"Component\" and newattrs['__module__'] != 'lbry.testcase':\n            ComponentManager.default_component_classes[klass.component_name] = klass\n        return klass\n\n\nclass Component(metaclass=ComponentType):\n    \"\"\"\n    lbry-daemon component helper\n\n    Inheriting classes will be automatically registered with the ComponentManager and must implement setup and stop\n    methods\n    \"\"\"\n\n    depends_on = []\n    component_name = None\n\n    def __init__(self, component_manager):\n        self.conf: Config = component_manager.conf\n        self.component_manager = component_manager\n        self._running = False\n\n    def __lt__(self, other):\n        return self.component_name < other.component_name\n\n    @property\n    def running(self):\n        return self._running\n\n    async def get_status(self): # pylint: disable=no-self-use\n        return\n\n    async def start(self):\n        raise NotImplementedError()\n\n    async def stop(self):\n        raise NotImplementedError()\n\n    @property\n    def component(self):\n        raise NotImplementedError()\n\n    async def _setup(self):\n        try:\n            result = await self.start()\n            self._running = True\n            return result\n        except asyncio.CancelledError:\n            log.info(\"Cancelled setup of %s component\", self.__class__.__name__)\n            raise\n        except Exception as err:\n            log.exception(\"Error setting up %s\", self.component_name or self.__class__.__name__)\n            raise err\n\n    async def _stop(self):\n        try:\n            result = await self.stop()\n            self._running = False\n            return result\n        except asyncio.CancelledError:\n            log.info(\"Cancelled stop of %s component\", self.__class__.__name__)\n            raise\n        except Exception as err:\n            log.exception(\"Error stopping %s\", self.__class__.__name__)\n            raise err\n"
  },
  {
    "path": "lbry/extras/daemon/componentmanager.py",
    "content": "import logging\nimport asyncio\nfrom lbry.conf import Config\nfrom lbry.error import ComponentStartConditionNotMetError\nfrom lbry.dht.peer import PeerManager\n\nlog = logging.getLogger(__name__)\n\n\nclass RegisteredConditions:\n    conditions = {}\n\n\nclass RequiredConditionType(type):\n    def __new__(mcs, name, bases, newattrs):\n        klass = type.__new__(mcs, name, bases, newattrs)\n        if name != \"RequiredCondition\":\n            if klass.name in RegisteredConditions.conditions:\n                raise SyntaxError(\"already have a component registered for \\\"%s\\\"\" % klass.name)\n            RegisteredConditions.conditions[klass.name] = klass\n        return klass\n\n\nclass RequiredCondition(metaclass=RequiredConditionType):\n    name = \"\"\n    component = \"\"\n    message = \"\"\n\n    @staticmethod\n    def evaluate(component):\n        raise NotImplementedError()\n\n\nclass ComponentManager:\n    default_component_classes = {}\n\n    def __init__(self, conf: Config, analytics_manager=None, skip_components=None,\n                 peer_manager=None, **override_components):\n        self.conf = conf\n        self.skip_components = skip_components or []\n        self.loop = asyncio.get_event_loop()\n        self.analytics_manager = analytics_manager\n        self.component_classes = {}\n        self.components = set()\n        self.started = asyncio.Event()\n        self.peer_manager = peer_manager or PeerManager(asyncio.get_event_loop_policy().get_event_loop())\n\n        for component_name, component_class in self.default_component_classes.items():\n            if component_name in override_components:\n                component_class = override_components.pop(component_name)\n            if component_name not in self.skip_components:\n                self.component_classes[component_name] = component_class\n\n        if override_components:\n            raise SyntaxError(\"unexpected components: %s\" % override_components)\n\n        for component_class in self.component_classes.values():\n            self.components.add(component_class(self))\n\n    def evaluate_condition(self, condition_name):\n        if condition_name not in RegisteredConditions.conditions:\n            raise NameError(condition_name)\n        condition = RegisteredConditions.conditions[condition_name]\n        try:\n            component = self.get_component(condition.component)\n            result = condition.evaluate(component)\n        except Exception:\n            log.exception('failed to evaluate condition:')\n            result = False\n        return result, \"\" if result else condition.message\n\n    def sort_components(self, reverse=False):\n        \"\"\"\n        Sort components by requirements\n        \"\"\"\n        steps = []\n        staged = set()\n        components = set(self.components)\n\n        # components with no requirements\n        step = []\n        for component in set(components):\n            if not component.depends_on:\n                step.append(component)\n                staged.add(component.component_name)\n                components.remove(component)\n\n        if step:\n            step.sort()\n            steps.append(step)\n\n        while components:\n            step = []\n            to_stage = set()\n            for component in set(components):\n                reqs_met = 0\n                for needed in component.depends_on:\n                    if needed in staged:\n                        reqs_met += 1\n                if reqs_met == len(component.depends_on):\n                    step.append(component)\n                    to_stage.add(component.component_name)\n                    components.remove(component)\n            if step:\n                step.sort()\n                staged.update(to_stage)\n                steps.append(step)\n            elif components:\n                raise ComponentStartConditionNotMetError(components)\n        if reverse:\n            steps.reverse()\n        return steps\n\n    async def start(self):\n        \"\"\" Start Components in sequence sorted by requirements \"\"\"\n        for stage in self.sort_components():\n            needing_start = [\n                component._setup() for component in stage if not component.running\n            ]\n            if needing_start:\n                await asyncio.wait(map(asyncio.create_task, needing_start))\n        self.started.set()\n\n    async def stop(self):\n        \"\"\"\n        Stop Components in reversed startup order\n        \"\"\"\n        stages = self.sort_components(reverse=True)\n        for stage in stages:\n            needing_stop = [\n                component._stop() for component in stage if component.running\n            ]\n            if needing_stop:\n                await asyncio.wait(map(asyncio.create_task, needing_stop))\n\n    def all_components_running(self, *component_names):\n        \"\"\"\n        Check if components are running\n\n        :return: (bool) True if all specified components are running\n        \"\"\"\n        components = {component.component_name: component for component in self.components}\n        for component in component_names:\n            if component not in components:\n                raise NameError(\"%s is not a known Component\" % component)\n            if not components[component].running:\n                return False\n        return True\n\n    def get_components_status(self):\n        \"\"\"\n        List status of all the components, whether they are running or not\n\n        :return: (dict) {(str) component_name: (bool) True is running else False}\n        \"\"\"\n        return {\n            component.component_name: component.running\n            for component in self.components\n        }\n\n    def get_actual_component(self, component_name):\n        for component in self.components:\n            if component.component_name == component_name:\n                return component\n        raise NameError(component_name)\n\n    def get_component(self, component_name):\n        return self.get_actual_component(component_name).component\n\n    def has_component(self, component_name):\n        return any(component for component in self.components if component_name == component.component_name)\n"
  },
  {
    "path": "lbry/extras/daemon/components.py",
    "content": "import math\nimport os\nimport asyncio\nimport logging\nimport binascii\nimport typing\n\nimport base58\n\nfrom aioupnp import __version__ as aioupnp_version\nfrom aioupnp.upnp import UPnP\nfrom aioupnp.fault import UPnPError\n\nfrom lbry import utils\nfrom lbry.dht.node import Node\nfrom lbry.dht.peer import is_valid_public_ipv4\nfrom lbry.dht.blob_announcer import BlobAnnouncer\nfrom lbry.blob.blob_manager import BlobManager\nfrom lbry.blob.disk_space_manager import DiskSpaceManager\nfrom lbry.blob_exchange.server import BlobServer\nfrom lbry.stream.background_downloader import BackgroundDownloader\nfrom lbry.stream.stream_manager import StreamManager\nfrom lbry.file.file_manager import FileManager\nfrom lbry.extras.daemon.component import Component\nfrom lbry.extras.daemon.exchange_rate_manager import ExchangeRateManager\nfrom lbry.extras.daemon.storage import SQLiteStorage\nfrom lbry.torrent.torrent_manager import TorrentManager\nfrom lbry.wallet import WalletManager\nfrom lbry.wallet.usage_payment import WalletServerPayer\nfrom lbry.torrent.tracker import TrackerClient\nfrom lbry.torrent.session import TorrentSession\n\nlog = logging.getLogger(__name__)\n\n# settings must be initialized before this file is imported\n\nDATABASE_COMPONENT = \"database\"\nBLOB_COMPONENT = \"blob_manager\"\nWALLET_COMPONENT = \"wallet\"\nWALLET_SERVER_PAYMENTS_COMPONENT = \"wallet_server_payments\"\nDHT_COMPONENT = \"dht\"\nHASH_ANNOUNCER_COMPONENT = \"hash_announcer\"\nFILE_MANAGER_COMPONENT = \"file_manager\"\nDISK_SPACE_COMPONENT = \"disk_space\"\nBACKGROUND_DOWNLOADER_COMPONENT = \"background_downloader\"\nPEER_PROTOCOL_SERVER_COMPONENT = \"peer_protocol_server\"\nUPNP_COMPONENT = \"upnp\"\nEXCHANGE_RATE_MANAGER_COMPONENT = \"exchange_rate_manager\"\nTRACKER_ANNOUNCER_COMPONENT = \"tracker_announcer_component\"\nLIBTORRENT_COMPONENT = \"libtorrent_component\"\n\n\nclass DatabaseComponent(Component):\n    component_name = DATABASE_COMPONENT\n\n    def __init__(self, component_manager):\n        super().__init__(component_manager)\n        self.storage = None\n\n    @property\n    def component(self):\n        return self.storage\n\n    @staticmethod\n    def get_current_db_revision():\n        return 15\n\n    @property\n    def revision_filename(self):\n        return os.path.join(self.conf.data_dir, 'db_revision')\n\n    def _write_db_revision_file(self, version_num):\n        with open(self.revision_filename, mode='w') as db_revision:\n            db_revision.write(str(version_num))\n\n    async def start(self):\n        # check directories exist, create them if they don't\n        log.info(\"Loading databases\")\n\n        if not os.path.exists(self.revision_filename):\n            log.info(\"db_revision file not found. Creating it\")\n            self._write_db_revision_file(self.get_current_db_revision())\n\n        # check the db migration and run any needed migrations\n        with open(self.revision_filename, \"r\") as revision_read_handle:\n            old_revision = int(revision_read_handle.read().strip())\n\n        if old_revision > self.get_current_db_revision():\n            raise Exception('This version of lbrynet is not compatible with the database\\n'\n                            'Your database is revision %i, expected %i' %\n                            (old_revision, self.get_current_db_revision()))\n        if old_revision < self.get_current_db_revision():\n            from lbry.extras.daemon.migrator import dbmigrator  # pylint: disable=import-outside-toplevel\n            log.info(\"Upgrading your databases (revision %i to %i)\", old_revision, self.get_current_db_revision())\n            await asyncio.get_event_loop().run_in_executor(\n                None, dbmigrator.migrate_db, self.conf, old_revision, self.get_current_db_revision()\n            )\n            self._write_db_revision_file(self.get_current_db_revision())\n            log.info(\"Finished upgrading the databases.\")\n\n        self.storage = SQLiteStorage(\n            self.conf, os.path.join(self.conf.data_dir, \"lbrynet.sqlite\")\n        )\n        await self.storage.open()\n\n    async def stop(self):\n        await self.storage.close()\n        self.storage = None\n\n\nclass WalletComponent(Component):\n    component_name = WALLET_COMPONENT\n    depends_on = [DATABASE_COMPONENT]\n\n    def __init__(self, component_manager):\n        super().__init__(component_manager)\n        self.wallet_manager = None\n\n    @property\n    def component(self):\n        return self.wallet_manager\n\n    async def get_status(self):\n        if self.wallet_manager is None:\n            return\n        is_connected = self.wallet_manager.ledger.network.is_connected\n        sessions = []\n        connected = None\n        if is_connected:\n            addr, port = self.wallet_manager.ledger.network.client.server\n            connected = f\"{addr}:{port}\"\n            sessions.append(self.wallet_manager.ledger.network.client)\n\n        result = {\n            'connected': connected,\n            'connected_features': self.wallet_manager.ledger.network.server_features,\n            'servers': [\n                {\n                    'host': session.server[0],\n                    'port': session.server[1],\n                    'latency': session.connection_latency,\n                    'availability': session.available,\n                } for session in sessions\n            ],\n            'known_servers': len(self.wallet_manager.ledger.network.known_hubs),\n            'available_servers': 1 if is_connected else 0\n        }\n\n        if self.wallet_manager.ledger.network.remote_height:\n            local_height = self.wallet_manager.ledger.local_height_including_downloaded_height\n            disk_height = len(self.wallet_manager.ledger.headers)\n            remote_height = self.wallet_manager.ledger.network.remote_height\n            download_height, target_height = local_height - disk_height, remote_height - disk_height\n            if target_height > 0:\n                progress = min(max(math.ceil(float(download_height) / float(target_height) * 100), 0), 100)\n            else:\n                progress = 100\n            best_hash = await self.wallet_manager.get_best_blockhash()\n            result.update({\n                'headers_synchronization_progress': progress,\n                'blocks': max(local_height, 0),\n                'blocks_behind': max(remote_height - local_height, 0),\n                'best_blockhash': best_hash,\n            })\n\n        return result\n\n    async def start(self):\n        log.info(\"Starting wallet\")\n        self.wallet_manager = await WalletManager.from_lbrynet_config(self.conf)\n        await self.wallet_manager.start()\n\n    async def stop(self):\n        await self.wallet_manager.stop()\n        self.wallet_manager = None\n\n\nclass WalletServerPaymentsComponent(Component):\n    component_name = WALLET_SERVER_PAYMENTS_COMPONENT\n    depends_on = [WALLET_COMPONENT]\n\n    def __init__(self, component_manager):\n        super().__init__(component_manager)\n        self.usage_payment_service = WalletServerPayer(\n            max_fee=self.conf.max_wallet_server_fee, analytics_manager=self.component_manager.analytics_manager,\n        )\n\n    @property\n    def component(self) -> typing.Optional[WalletServerPayer]:\n        return self.usage_payment_service\n\n    async def start(self):\n        wallet_manager = self.component_manager.get_component(WALLET_COMPONENT)\n        await self.usage_payment_service.start(wallet_manager.ledger, wallet_manager.default_wallet)\n\n    async def stop(self):\n        await self.usage_payment_service.stop()\n\n    async def get_status(self):\n        return {\n            'max_fee': self.usage_payment_service.max_fee,\n            'running': self.usage_payment_service.running\n        }\n\n\nclass BlobComponent(Component):\n    component_name = BLOB_COMPONENT\n    depends_on = [DATABASE_COMPONENT]\n\n    def __init__(self, component_manager):\n        super().__init__(component_manager)\n        self.blob_manager: typing.Optional[BlobManager] = None\n\n    @property\n    def component(self) -> typing.Optional[BlobManager]:\n        return self.blob_manager\n\n    async def start(self):\n        storage = self.component_manager.get_component(DATABASE_COMPONENT)\n        data_store = None\n        if DHT_COMPONENT not in self.component_manager.skip_components:\n            dht_node: Node = self.component_manager.get_component(DHT_COMPONENT)\n            if dht_node:\n                data_store = dht_node.protocol.data_store\n        blob_dir = os.path.join(self.conf.data_dir, 'blobfiles')\n        if not os.path.isdir(blob_dir):\n            os.mkdir(blob_dir)\n        self.blob_manager = BlobManager(self.component_manager.loop, blob_dir, storage, self.conf, data_store)\n        return await self.blob_manager.setup()\n\n    async def stop(self):\n        self.blob_manager.stop()\n\n    async def get_status(self):\n        count = 0\n        if self.blob_manager:\n            count = len(self.blob_manager.completed_blob_hashes)\n        return {\n            'finished_blobs': count,\n            'connections': {} if not self.blob_manager else self.blob_manager.connection_manager.status\n        }\n\n\nclass DHTComponent(Component):\n    component_name = DHT_COMPONENT\n    depends_on = [UPNP_COMPONENT, DATABASE_COMPONENT]\n\n    def __init__(self, component_manager):\n        super().__init__(component_manager)\n        self.dht_node: typing.Optional[Node] = None\n        self.external_udp_port = None\n        self.external_peer_port = None\n\n    @property\n    def component(self) -> typing.Optional[Node]:\n        return self.dht_node\n\n    async def get_status(self):\n        return {\n            'node_id': None if not self.dht_node else binascii.hexlify(self.dht_node.protocol.node_id),\n            'peers_in_routing_table': 0 if not self.dht_node else len(self.dht_node.protocol.routing_table.get_peers())\n        }\n\n    def get_node_id(self):\n        node_id_filename = os.path.join(self.conf.data_dir, \"node_id\")\n        if os.path.isfile(node_id_filename):\n            with open(node_id_filename, \"r\") as node_id_file:\n                return base58.b58decode(str(node_id_file.read()).strip())\n        node_id = utils.generate_id()\n        with open(node_id_filename, \"w\") as node_id_file:\n            node_id_file.write(base58.b58encode(node_id).decode())\n        return node_id\n\n    async def start(self):\n        log.info(\"start the dht\")\n        upnp_component = self.component_manager.get_component(UPNP_COMPONENT)\n        self.external_peer_port = upnp_component.upnp_redirects.get(\"TCP\", self.conf.tcp_port)\n        self.external_udp_port = upnp_component.upnp_redirects.get(\"UDP\", self.conf.udp_port)\n        external_ip = upnp_component.external_ip\n        storage = self.component_manager.get_component(DATABASE_COMPONENT)\n        if not external_ip:\n            external_ip, _ = await utils.get_external_ip(self.conf.lbryum_servers)\n            if not external_ip:\n                log.warning(\"failed to get external ip\")\n\n        self.dht_node = Node(\n            self.component_manager.loop,\n            self.component_manager.peer_manager,\n            node_id=self.get_node_id(),\n            internal_udp_port=self.conf.udp_port,\n            udp_port=self.external_udp_port,\n            external_ip=external_ip,\n            peer_port=self.external_peer_port,\n            rpc_timeout=self.conf.node_rpc_timeout,\n            split_buckets_under_index=self.conf.split_buckets_under_index,\n            is_bootstrap_node=self.conf.is_bootstrap_node,\n            storage=storage\n        )\n        self.dht_node.start(self.conf.network_interface, self.conf.known_dht_nodes)\n        log.info(\"Started the dht\")\n\n    async def stop(self):\n        self.dht_node.stop()\n\n\nclass HashAnnouncerComponent(Component):\n    component_name = HASH_ANNOUNCER_COMPONENT\n    depends_on = [DHT_COMPONENT, DATABASE_COMPONENT]\n\n    def __init__(self, component_manager):\n        super().__init__(component_manager)\n        self.hash_announcer: typing.Optional[BlobAnnouncer] = None\n\n    @property\n    def component(self) -> typing.Optional[BlobAnnouncer]:\n        return self.hash_announcer\n\n    async def start(self):\n        storage = self.component_manager.get_component(DATABASE_COMPONENT)\n        dht_node = self.component_manager.get_component(DHT_COMPONENT)\n        self.hash_announcer = BlobAnnouncer(self.component_manager.loop, dht_node, storage)\n        self.hash_announcer.start(self.conf.concurrent_blob_announcers)\n        log.info(\"Started blob announcer\")\n\n    async def stop(self):\n        self.hash_announcer.stop()\n        log.info(\"Stopped blob announcer\")\n\n    async def get_status(self):\n        return {\n            'announce_queue_size': 0 if not self.hash_announcer else len(self.hash_announcer.announce_queue)\n        }\n\n\nclass FileManagerComponent(Component):\n    component_name = FILE_MANAGER_COMPONENT\n    depends_on = [BLOB_COMPONENT, DATABASE_COMPONENT, WALLET_COMPONENT]\n\n    def __init__(self, component_manager):\n        super().__init__(component_manager)\n        self.file_manager: typing.Optional[FileManager] = None\n\n    @property\n    def component(self) -> typing.Optional[FileManager]:\n        return self.file_manager\n\n    async def get_status(self):\n        if not self.file_manager:\n            return\n        return {\n            'managed_files': len(self.file_manager.get_filtered()),\n        }\n\n    async def start(self):\n        blob_manager = self.component_manager.get_component(BLOB_COMPONENT)\n        storage = self.component_manager.get_component(DATABASE_COMPONENT)\n        wallet = self.component_manager.get_component(WALLET_COMPONENT)\n        node = self.component_manager.get_component(DHT_COMPONENT) \\\n            if self.component_manager.has_component(DHT_COMPONENT) else None\n        log.info('Starting the file manager')\n        loop = asyncio.get_event_loop()\n        self.file_manager = FileManager(\n            loop, self.conf, wallet, storage, self.component_manager.analytics_manager\n        )\n        self.file_manager.source_managers['stream'] = StreamManager(\n            loop, self.conf, blob_manager, wallet, storage, node,\n        )\n        if self.component_manager.has_component(LIBTORRENT_COMPONENT):\n            torrent = self.component_manager.get_component(LIBTORRENT_COMPONENT)\n            self.file_manager.source_managers['torrent'] = TorrentManager(\n                loop, self.conf, torrent, storage, self.component_manager.analytics_manager\n            )\n        await self.file_manager.start()\n        log.info('Done setting up file manager')\n\n    async def stop(self):\n        await self.file_manager.stop()\n\n\nclass BackgroundDownloaderComponent(Component):\n    MIN_PREFIX_COLLIDING_BITS = 8\n    component_name = BACKGROUND_DOWNLOADER_COMPONENT\n    depends_on = [DATABASE_COMPONENT, BLOB_COMPONENT, DISK_SPACE_COMPONENT]\n\n    def __init__(self, component_manager):\n        super().__init__(component_manager)\n        self.background_task: typing.Optional[asyncio.Task] = None\n        self.download_loop_delay_seconds = 60\n        self.ongoing_download: typing.Optional[asyncio.Task] = None\n        self.space_manager: typing.Optional[DiskSpaceManager] = None\n        self.blob_manager: typing.Optional[BlobManager] = None\n        self.background_downloader: typing.Optional[BackgroundDownloader] = None\n        self.dht_node: typing.Optional[Node] = None\n        self.space_available: typing.Optional[int] = None\n\n    @property\n    def is_busy(self):\n        return bool(self.ongoing_download and not self.ongoing_download.done())\n\n    @property\n    def component(self) -> 'BackgroundDownloaderComponent':\n        return self\n\n    async def get_status(self):\n        return {'running': self.background_task is not None and not self.background_task.done(),\n                'available_free_space_mb': self.space_available,\n                'ongoing_download': self.is_busy}\n\n    async def download_blobs_in_background(self):\n        while True:\n            self.space_available = await self.space_manager.get_free_space_mb(True)\n            if not self.is_busy and self.space_available > 10:\n                self._download_next_close_blob_hash()\n            await asyncio.sleep(self.download_loop_delay_seconds)\n\n    def _download_next_close_blob_hash(self):\n        node_id = self.dht_node.protocol.node_id\n        for blob_hash in self.dht_node.stored_blob_hashes:\n            if blob_hash.hex() in self.blob_manager.completed_blob_hashes:\n                continue\n            if utils.get_colliding_prefix_bits(node_id, blob_hash) >= self.MIN_PREFIX_COLLIDING_BITS:\n                self.ongoing_download = asyncio.create_task(self.background_downloader.download_blobs(blob_hash.hex()))\n                return\n\n    async def start(self):\n        self.space_manager: DiskSpaceManager = self.component_manager.get_component(DISK_SPACE_COMPONENT)\n        if not self.component_manager.has_component(DHT_COMPONENT):\n            return\n        self.dht_node = self.component_manager.get_component(DHT_COMPONENT)\n        self.blob_manager = self.component_manager.get_component(BLOB_COMPONENT)\n        storage = self.component_manager.get_component(DATABASE_COMPONENT)\n        self.background_downloader = BackgroundDownloader(self.conf, storage, self.blob_manager, self.dht_node)\n        self.background_task = asyncio.create_task(self.download_blobs_in_background())\n\n    async def stop(self):\n        if self.ongoing_download and not self.ongoing_download.done():\n            self.ongoing_download.cancel()\n        if self.background_task:\n            self.background_task.cancel()\n\n\nclass DiskSpaceComponent(Component):\n    component_name = DISK_SPACE_COMPONENT\n    depends_on = [DATABASE_COMPONENT, BLOB_COMPONENT]\n\n    def __init__(self, component_manager):\n        super().__init__(component_manager)\n        self.disk_space_manager: typing.Optional[DiskSpaceManager] = None\n\n    @property\n    def component(self) -> typing.Optional[DiskSpaceManager]:\n        return self.disk_space_manager\n\n    async def get_status(self):\n        if self.disk_space_manager:\n            space_used = await self.disk_space_manager.get_space_used_mb(cached=True)\n            return {\n                'total_used_mb': space_used['total'],\n                'published_blobs_storage_used_mb': space_used['private_storage'],\n                'content_blobs_storage_used_mb': space_used['content_storage'],\n                'seed_blobs_storage_used_mb': space_used['network_storage'],\n                'running': self.disk_space_manager.running,\n            }\n        return {'space_used': '0', 'network_seeding_space_used': '0', 'running': False}\n\n    async def start(self):\n        db = self.component_manager.get_component(DATABASE_COMPONENT)\n        blob_manager = self.component_manager.get_component(BLOB_COMPONENT)\n        self.disk_space_manager = DiskSpaceManager(\n            self.conf, db, blob_manager,\n            analytics=self.component_manager.analytics_manager\n        )\n        await self.disk_space_manager.start()\n\n    async def stop(self):\n        await self.disk_space_manager.stop()\n\n\nclass TorrentComponent(Component):\n    component_name = LIBTORRENT_COMPONENT\n\n    def __init__(self, component_manager):\n        super().__init__(component_manager)\n        self.torrent_session = None\n\n    @property\n    def component(self) -> typing.Optional[TorrentSession]:\n        return self.torrent_session\n\n    async def get_status(self):\n        if not self.torrent_session:\n            return\n        return {\n            'running': True,  # TODO: what to return here?\n        }\n\n    async def start(self):\n        self.torrent_session = TorrentSession(asyncio.get_event_loop(), None)\n        await self.torrent_session.bind()  # TODO: specify host/port\n\n    async def stop(self):\n        if self.torrent_session:\n            await self.torrent_session.pause()\n\n\nclass PeerProtocolServerComponent(Component):\n    component_name = PEER_PROTOCOL_SERVER_COMPONENT\n    depends_on = [UPNP_COMPONENT, BLOB_COMPONENT, WALLET_COMPONENT]\n\n    def __init__(self, component_manager):\n        super().__init__(component_manager)\n        self.blob_server: typing.Optional[BlobServer] = None\n\n    @property\n    def component(self) -> typing.Optional[BlobServer]:\n        return self.blob_server\n\n    async def start(self):\n        log.info(\"start blob server\")\n        blob_manager: BlobManager = self.component_manager.get_component(BLOB_COMPONENT)\n        wallet: WalletManager = self.component_manager.get_component(WALLET_COMPONENT)\n        peer_port = self.conf.tcp_port\n        address = await wallet.get_unused_address()\n        self.blob_server = BlobServer(asyncio.get_event_loop(), blob_manager, address)\n        self.blob_server.start_server(peer_port, interface=self.conf.network_interface)\n        await self.blob_server.started_listening.wait()\n\n    async def stop(self):\n        if self.blob_server:\n            self.blob_server.stop_server()\n\n\nclass UPnPComponent(Component):\n    component_name = UPNP_COMPONENT\n\n    def __init__(self, component_manager):\n        super().__init__(component_manager)\n        self._int_peer_port = self.conf.tcp_port\n        self._int_dht_node_port = self.conf.udp_port\n        self.use_upnp = self.conf.use_upnp\n        self.upnp: typing.Optional[UPnP] = None\n        self.upnp_redirects = {}\n        self.external_ip: typing.Optional[str] = None\n        self._maintain_redirects_task = None\n\n    @property\n    def component(self) -> 'UPnPComponent':\n        return self\n\n    async def _repeatedly_maintain_redirects(self, now=True):\n        while True:\n            if now:\n                await self._maintain_redirects()\n            await asyncio.sleep(360)\n\n    async def _maintain_redirects(self):\n        # setup the gateway if necessary\n        if not self.upnp:\n            try:\n                self.upnp = await UPnP.discover(loop=self.component_manager.loop)\n                log.info(\"found upnp gateway: %s\", self.upnp.gateway.manufacturer_string)\n            except Exception as err:\n                log.warning(\"upnp discovery failed: %s\", err)\n                self.upnp = None\n\n        # update the external ip\n        external_ip = None\n        if self.upnp:\n            try:\n                external_ip = await self.upnp.get_external_ip()\n                if external_ip != \"0.0.0.0\" and not self.external_ip:\n                    log.info(\"got external ip from UPnP: %s\", external_ip)\n            except (asyncio.TimeoutError, UPnPError, NotImplementedError):\n                pass\n        if external_ip and not is_valid_public_ipv4(external_ip):\n            log.warning(\"UPnP returned a private/reserved ip - %s, checking lbry.com fallback\", external_ip)\n            external_ip, _ = await utils.get_external_ip(self.conf.lbryum_servers)\n        if self.external_ip and self.external_ip != external_ip:\n            log.info(\"external ip changed from %s to %s\", self.external_ip, external_ip)\n        if external_ip:\n            self.external_ip = external_ip\n            dht_component = self.component_manager.get_component(DHT_COMPONENT)\n            if dht_component:\n                dht_node = dht_component.component\n                dht_node.protocol.external_ip = external_ip\n        # assert self.external_ip is not None   # TODO: handle going/starting offline\n\n        if not self.upnp_redirects and self.upnp:  # setup missing redirects\n            log.info(\"add UPnP port mappings\")\n            upnp_redirects = {}\n            if PEER_PROTOCOL_SERVER_COMPONENT not in self.component_manager.skip_components:\n                try:\n                    upnp_redirects[\"TCP\"] = await self.upnp.get_next_mapping(\n                        self._int_peer_port, \"TCP\", \"LBRY peer port\", self._int_peer_port\n                    )\n                except (UPnPError, asyncio.TimeoutError, NotImplementedError):\n                    pass\n            if DHT_COMPONENT not in self.component_manager.skip_components:\n                try:\n                    upnp_redirects[\"UDP\"] = await self.upnp.get_next_mapping(\n                        self._int_dht_node_port, \"UDP\", \"LBRY DHT port\", self._int_dht_node_port\n                    )\n                except (UPnPError, asyncio.TimeoutError, NotImplementedError):\n                    pass\n            if upnp_redirects:\n                log.info(\"set up redirects: %s\", upnp_redirects)\n                self.upnp_redirects.update(upnp_redirects)\n        elif self.upnp:  # check existing redirects are still active\n            found = set()\n            mappings = await self.upnp.get_redirects()\n            for mapping in mappings:\n                proto = mapping.protocol\n                if proto in self.upnp_redirects and mapping.external_port == self.upnp_redirects[proto]:\n                    if mapping.lan_address == self.upnp.lan_address:\n                        found.add(proto)\n            if 'UDP' not in found and DHT_COMPONENT not in self.component_manager.skip_components:\n                try:\n                    udp_port = await self.upnp.get_next_mapping(self._int_dht_node_port, \"UDP\", \"LBRY DHT port\")\n                    self.upnp_redirects['UDP'] = udp_port\n                    log.info(\"refreshed upnp redirect for dht port: %i\", udp_port)\n                except (asyncio.TimeoutError, UPnPError, NotImplementedError):\n                    del self.upnp_redirects['UDP']\n            if 'TCP' not in found and PEER_PROTOCOL_SERVER_COMPONENT not in self.component_manager.skip_components:\n                try:\n                    tcp_port = await self.upnp.get_next_mapping(self._int_peer_port, \"TCP\", \"LBRY peer port\")\n                    self.upnp_redirects['TCP'] = tcp_port\n                    log.info(\"refreshed upnp redirect for peer port: %i\", tcp_port)\n                except (asyncio.TimeoutError, UPnPError, NotImplementedError):\n                    del self.upnp_redirects['TCP']\n            if ('TCP' in self.upnp_redirects and\n                    PEER_PROTOCOL_SERVER_COMPONENT not in self.component_manager.skip_components) and \\\n                    ('UDP' in self.upnp_redirects and DHT_COMPONENT not in self.component_manager.skip_components):\n                if self.upnp_redirects:\n                    log.debug(\"upnp redirects are still active\")\n\n    async def start(self):\n        log.info(\"detecting external ip\")\n        if not self.use_upnp:\n            self.external_ip, _ = await utils.get_external_ip(self.conf.lbryum_servers)\n            return\n        success = False\n        await self._maintain_redirects()\n        if self.upnp:\n            if not self.upnp_redirects and not all(\n                    x in self.component_manager.skip_components\n                    for x in (DHT_COMPONENT, PEER_PROTOCOL_SERVER_COMPONENT)\n            ):\n                log.error(\"failed to setup upnp\")\n            else:\n                success = True\n                if self.upnp_redirects:\n                    log.debug(\"set up upnp port redirects for gateway: %s\", self.upnp.gateway.manufacturer_string)\n        else:\n            log.error(\"failed to setup upnp\")\n        if not self.external_ip:\n            self.external_ip, probed_url = await utils.get_external_ip(self.conf.lbryum_servers)\n            if self.external_ip:\n                log.info(\"detected external ip using %s fallback\", probed_url)\n        if self.component_manager.analytics_manager:\n            self.component_manager.loop.create_task(\n                self.component_manager.analytics_manager.send_upnp_setup_success_fail(\n                    success, await self.get_status()\n                )\n            )\n        self._maintain_redirects_task = self.component_manager.loop.create_task(\n            self._repeatedly_maintain_redirects(now=False)\n        )\n\n    async def stop(self):\n        if self.upnp_redirects:\n            log.info(\"Removing upnp redirects: %s\", self.upnp_redirects)\n            await asyncio.wait([\n                self.upnp.delete_port_mapping(port, protocol) for protocol, port in self.upnp_redirects.items()\n            ])\n        if self._maintain_redirects_task and not self._maintain_redirects_task.done():\n            self._maintain_redirects_task.cancel()\n\n    async def get_status(self):\n        return {\n            'aioupnp_version': aioupnp_version,\n            'redirects': self.upnp_redirects,\n            'gateway': 'No gateway found' if not self.upnp else self.upnp.gateway.manufacturer_string,\n            'dht_redirect_set': 'UDP' in self.upnp_redirects,\n            'peer_redirect_set': 'TCP' in self.upnp_redirects,\n            'external_ip': self.external_ip\n        }\n\n\nclass ExchangeRateManagerComponent(Component):\n    component_name = EXCHANGE_RATE_MANAGER_COMPONENT\n\n    def __init__(self, component_manager):\n        super().__init__(component_manager)\n        self.exchange_rate_manager = ExchangeRateManager()\n\n    @property\n    def component(self) -> ExchangeRateManager:\n        return self.exchange_rate_manager\n\n    async def start(self):\n        self.exchange_rate_manager.start()\n\n    async def stop(self):\n        self.exchange_rate_manager.stop()\n\n\nclass TrackerAnnouncerComponent(Component):\n    component_name = TRACKER_ANNOUNCER_COMPONENT\n    depends_on = [FILE_MANAGER_COMPONENT]\n\n    def __init__(self, component_manager):\n        super().__init__(component_manager)\n        self.file_manager = None\n        self.announce_task = None\n        self.tracker_client: typing.Optional[TrackerClient] = None\n\n    @property\n    def component(self):\n        return self.tracker_client\n\n    @property\n    def running(self):\n        return self._running and self.announce_task and not self.announce_task.done()\n\n    async def announce_forever(self):\n        while True:\n            sleep_seconds = 60.0\n            announce_sd_hashes = []\n            for file in self.file_manager.get_filtered():\n                if not file.downloader:\n                    continue\n                announce_sd_hashes.append(bytes.fromhex(file.sd_hash))\n            await self.tracker_client.announce_many(*announce_sd_hashes)\n            await asyncio.sleep(sleep_seconds)\n\n    async def start(self):\n        node = self.component_manager.get_component(DHT_COMPONENT) \\\n            if self.component_manager.has_component(DHT_COMPONENT) else None\n        node_id = node.protocol.node_id if node else None\n        self.tracker_client = TrackerClient(node_id, self.conf.tcp_port, lambda: self.conf.tracker_servers)\n        await self.tracker_client.start()\n        self.file_manager = self.component_manager.get_component(FILE_MANAGER_COMPONENT)\n        self.announce_task = asyncio.create_task(self.announce_forever())\n\n    async def stop(self):\n        self.file_manager = None\n        if self.announce_task and not self.announce_task.done():\n            self.announce_task.cancel()\n        self.announce_task = None\n        self.tracker_client.stop()\n"
  },
  {
    "path": "lbry/extras/daemon/daemon.py",
    "content": "import linecache\nimport os\nimport re\nimport asyncio\nimport logging\nimport json\nimport time\nimport inspect\nimport typing\nimport random\nimport tracemalloc\nimport itertools\nfrom urllib.parse import urlencode, quote\nfrom typing import Callable, Optional, List\nfrom binascii import hexlify, unhexlify\nfrom traceback import format_exc\nfrom functools import wraps, partial\n\nimport base58\nfrom aiohttp import web\nfrom prometheus_client import generate_latest as prom_generate_latest, Gauge, Histogram, Counter\nfrom google.protobuf.message import DecodeError\n\nfrom lbry.wallet import (\n    Wallet, ENCRYPT_ON_DISK, SingleKey, HierarchicalDeterministic,\n    Transaction, Output, Input, Account, database\n)\nfrom lbry.wallet.dewies import dewies_to_lbc, lbc_to_dewies, dict_values_to_lbc\nfrom lbry.wallet.constants import TXO_TYPES, CLAIM_TYPE_NAMES\nfrom lbry.wallet.bip32 import PrivateKey\nfrom lbry.crypto.base58 import Base58\n\nfrom lbry import utils\nfrom lbry.conf import Config, Setting, NOT_SET\nfrom lbry.blob.blob_file import is_valid_blobhash, BlobBuffer\nfrom lbry.blob_exchange.downloader import download_blob\nfrom lbry.dht.peer import make_kademlia_peer\nfrom lbry.error import (\n    DownloadSDTimeoutError, ComponentsNotStartedError, ComponentStartConditionNotMetError,\n    CommandDoesNotExistError, BaseError, WalletNotFoundError, WalletAlreadyLoadedError, WalletAlreadyExistsError,\n    ConflictingInputValueError, AlreadyPurchasedError, PrivateKeyNotFoundError, InputStringIsBlankError,\n    InputValueError\n)\nfrom lbry.extras import system_info\nfrom lbry.extras.daemon import analytics\nfrom lbry.extras.daemon.components import WALLET_COMPONENT, DATABASE_COMPONENT, DHT_COMPONENT, BLOB_COMPONENT\nfrom lbry.extras.daemon.components import FILE_MANAGER_COMPONENT, DISK_SPACE_COMPONENT, TRACKER_ANNOUNCER_COMPONENT\nfrom lbry.extras.daemon.components import EXCHANGE_RATE_MANAGER_COMPONENT, UPNP_COMPONENT\nfrom lbry.extras.daemon.componentmanager import RequiredCondition\nfrom lbry.extras.daemon.componentmanager import ComponentManager\nfrom lbry.extras.daemon.json_response_encoder import JSONResponseEncoder\nfrom lbry.extras.daemon.undecorated import undecorated\nfrom lbry.extras.daemon.security import ensure_request_allowed\nfrom lbry.file_analysis import VideoFileAnalyzer\nfrom lbry.schema.claim import Claim\nfrom lbry.schema.url import URL\n\n\nif typing.TYPE_CHECKING:\n    from lbry.blob.blob_manager import BlobManager\n    from lbry.dht.node import Node\n    from lbry.extras.daemon.components import UPnPComponent, DiskSpaceManager\n    from lbry.extras.daemon.exchange_rate_manager import ExchangeRateManager\n    from lbry.extras.daemon.storage import SQLiteStorage\n    from lbry.wallet import WalletManager, Ledger\n    from lbry.file.file_manager import FileManager\n\nlog = logging.getLogger(__name__)\n\nRANGE_FIELDS = {\n    'height', 'creation_height', 'activation_height', 'expiration_height',\n    'timestamp', 'creation_timestamp', 'duration', 'release_time', 'fee_amount',\n    'tx_position', 'repost_count', 'limit_claims_per_channel',\n    'amount', 'effective_amount', 'support_amount',\n    'trending_score', 'censor_type', 'tx_num'\n}\nMY_RANGE_FIELDS = RANGE_FIELDS - {\"limit_claims_per_channel\"}\nREPLACEMENTS = {\n    'claim_name': 'normalized_name',\n    'name': 'normalized_name',\n    'txid': 'tx_id',\n    'nout': 'tx_nout',\n    'trending_group': 'trending_score',\n    'trending_mixed': 'trending_score',\n    'trending_global': 'trending_score',\n    'trending_local': 'trending_score',\n    'reposted': 'repost_count',\n    'stream_types': 'stream_type',\n    'media_types': 'media_type',\n    'valid_channel_signature': 'is_signature_valid'\n}\n\n\ndef is_transactional_function(name):\n    for action in ('create', 'update', 'abandon', 'send', 'fund'):\n        if action in name:\n            return True\n\n\ndef requires(*components, **conditions):\n    if conditions and [\"conditions\"] != list(conditions.keys()):\n        raise SyntaxError(\"invalid conditions argument\")\n    condition_names = conditions.get(\"conditions\", [])\n\n    def _wrap(method):\n        @wraps(method)\n        def _inner(*args, **kwargs):\n            component_manager = args[0].component_manager\n            for condition_name in condition_names:\n                condition_result, err_msg = component_manager.evaluate_condition(condition_name)\n                if not condition_result:\n                    raise ComponentStartConditionNotMetError(err_msg)\n            if not component_manager.all_components_running(*components):\n                raise ComponentsNotStartedError(\n                    f\"the following required components have not yet started: {json.dumps(components)}\"\n                )\n            return method(*args, **kwargs)\n\n        return _inner\n\n    return _wrap\n\n\ndef deprecated(new_command=None):\n    def _deprecated_wrapper(f):\n        f.new_command = new_command\n        f._deprecated = True\n        return f\n\n    return _deprecated_wrapper\n\n\nINITIALIZING_CODE = 'initializing'\n\n# TODO: make this consistent with the stages in Downloader.py\nDOWNLOAD_METADATA_CODE = 'downloading_metadata'\nDOWNLOAD_TIMEOUT_CODE = 'timeout'\nDOWNLOAD_RUNNING_CODE = 'running'\nDOWNLOAD_STOPPED_CODE = 'stopped'\nSTREAM_STAGES = [\n    (INITIALIZING_CODE, 'Initializing'),\n    (DOWNLOAD_METADATA_CODE, 'Downloading metadata'),\n    (DOWNLOAD_RUNNING_CODE, 'Started %s, got %s/%s blobs, stream status: %s'),\n    (DOWNLOAD_STOPPED_CODE, 'Paused stream'),\n    (DOWNLOAD_TIMEOUT_CODE, 'Stream timed out')\n]\n\nSHORT_ID_LEN = 20\nMAX_UPDATE_FEE_ESTIMATE = 0.3\nDEFAULT_PAGE_SIZE = 20\n\nVALID_FULL_CLAIM_ID = re.compile('[0-9a-fA-F]{40}')\n\n\ndef encode_pagination_doc(items):\n    return {\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\": [items],\n    }\n\n\nasync def paginate_rows(get_records: Callable, get_record_count: Optional[Callable],\n                        page: Optional[int], page_size: Optional[int], **constraints):\n    page = max(1, page or 1)\n    page_size = max(1, page_size or DEFAULT_PAGE_SIZE)\n    constraints.update({\n        \"offset\": page_size * (page - 1),\n        \"limit\": page_size\n    })\n    items = await get_records(**constraints)\n    result = {\"items\": items, \"page\": page, \"page_size\": page_size}\n    if get_record_count is not None:\n        total_items = await get_record_count(**constraints)\n        result[\"total_pages\"] = int((total_items + (page_size - 1)) / page_size)\n        result[\"total_items\"] = total_items\n    return result\n\n\ndef paginate_list(items: List, page: Optional[int], page_size: Optional[int]):\n    page = max(1, page or 1)\n    page_size = max(1, page_size or DEFAULT_PAGE_SIZE)\n    total_items = len(items)\n    offset = page_size * (page - 1)\n    subitems = []\n    if offset <= total_items:\n        subitems = items[offset:offset+page_size]\n    return {\n        \"items\": subitems,\n        \"total_pages\": int((total_items + (page_size - 1)) / page_size),\n        \"total_items\": total_items,\n        \"page\": page, \"page_size\": page_size\n    }\n\n\nDHT_HAS_CONTACTS = \"dht_has_contacts\"\n\n\nclass DHTHasContacts(RequiredCondition):\n    name = DHT_HAS_CONTACTS\n    component = DHT_COMPONENT\n    message = \"your node is not connected to the dht\"\n\n    @staticmethod\n    def evaluate(component):\n        return len(component.contacts) > 0\n\n\nclass JSONRPCError:\n    # http://www.jsonrpc.org/specification#error_object\n    CODE_PARSE_ERROR = -32700  # Invalid JSON. Error while parsing the JSON text.\n    CODE_INVALID_REQUEST = -32600  # The JSON sent is not a valid Request object.\n    CODE_METHOD_NOT_FOUND = -32601  # The method does not exist / is not available.\n    CODE_INVALID_PARAMS = -32602  # Invalid method parameter(s).\n    CODE_INTERNAL_ERROR = -32603  # Internal JSON-RPC error (I think this is like a 500?)\n    CODE_APPLICATION_ERROR = -32500  # Generic error with our app??\n    CODE_AUTHENTICATION_ERROR = -32501  # Authentication failed\n\n    MESSAGES = {\n        CODE_PARSE_ERROR: \"Parse Error. Data is not valid JSON.\",\n        CODE_INVALID_REQUEST: \"JSON data is not a valid Request\",\n        CODE_METHOD_NOT_FOUND: \"Method Not Found\",\n        CODE_INVALID_PARAMS: \"Invalid Params\",\n        CODE_INTERNAL_ERROR: \"Internal Error\",\n        CODE_AUTHENTICATION_ERROR: \"Authentication Failed\",\n    }\n\n    HTTP_CODES = {\n        CODE_INVALID_REQUEST: 400,\n        CODE_PARSE_ERROR: 400,\n        CODE_INVALID_PARAMS: 400,\n        CODE_METHOD_NOT_FOUND: 404,\n        CODE_INTERNAL_ERROR: 500,\n        CODE_APPLICATION_ERROR: 500,\n        CODE_AUTHENTICATION_ERROR: 401,\n    }\n\n    def __init__(self, code: int, message: str, data: dict = None):\n        assert code and isinstance(code, int), \"'code' must be an int\"\n        assert message and isinstance(message, str), \"'message' must be a string\"\n        assert data is None or isinstance(data, dict), \"'data' must be None or a dict\"\n        self.code = code\n        self.message = message\n        self.data = data or {}\n\n    def to_dict(self):\n        return {\n            'code': self.code,\n            'message': self.message,\n            'data': self.data,\n        }\n\n    @staticmethod\n    def filter_traceback(traceback):\n        result = []\n        if traceback is not None:\n            result = trace_lines = traceback.split(\"\\n\")\n            for i, t in enumerate(trace_lines):\n                if \"--- <exception caught here> ---\" in t:\n                    if len(trace_lines) > i + 1:\n                        result = [j for j in trace_lines[i + 1:] if j]\n                        break\n        return result\n\n    @classmethod\n    def create_command_exception(cls, command, args, kwargs, exception, traceback):\n        if 'password' in kwargs and isinstance(kwargs['password'], str):\n            kwargs['password'] = '*'*len(kwargs['password'])\n        return cls(\n            cls.CODE_APPLICATION_ERROR, str(exception), {\n                'name': exception.__class__.__name__,\n                'traceback': cls.filter_traceback(traceback),\n                'command': command,\n                'args': args,\n                'kwargs': kwargs,\n            }\n        )\n\n\nclass UnknownAPIMethodError(Exception):\n    pass\n\n\ndef jsonrpc_dumps_pretty(obj, **kwargs):\n    if isinstance(obj, JSONRPCError):\n        data = {\"jsonrpc\": \"2.0\", \"error\": obj.to_dict()}\n    else:\n        data = {\"jsonrpc\": \"2.0\", \"result\": obj}\n    return json.dumps(data, cls=JSONResponseEncoder, sort_keys=True, indent=2, **kwargs) + \"\\n\"\n\n\ndef trap(err, *to_trap):\n    err.trap(*to_trap)\n\n\nclass JSONRPCServerType(type):\n    def __new__(mcs, name, bases, newattrs):\n        klass = type.__new__(mcs, name, bases, newattrs)\n        klass.callable_methods = {}\n        klass.deprecated_methods = {}\n\n        for methodname in dir(klass):\n            if methodname.startswith(\"jsonrpc_\"):\n                method = getattr(klass, methodname)\n                if not hasattr(method, '_deprecated'):\n                    klass.callable_methods.update({methodname.split(\"jsonrpc_\")[1]: method})\n                else:\n                    klass.deprecated_methods.update({methodname.split(\"jsonrpc_\")[1]: method})\n        return klass\n\n\nHISTOGRAM_BUCKETS = (\n    .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')\n)\n\n\nclass Daemon(metaclass=JSONRPCServerType):\n    \"\"\"\n    LBRYnet daemon, a jsonrpc interface to lbry functions\n    \"\"\"\n    callable_methods: dict\n    deprecated_methods: dict\n\n    pending_requests_metric = Gauge(\n        \"pending_requests\", \"Number of running api requests\", namespace=\"daemon_api\",\n        labelnames=(\"method\",)\n    )\n\n    requests_count_metric = Counter(\n        \"requests_count\", \"Number of requests received\", namespace=\"daemon_api\",\n        labelnames=(\"method\",)\n    )\n    failed_request_metric = Counter(\n        \"failed_request_count\", \"Number of failed requests\", namespace=\"daemon_api\",\n        labelnames=(\"method\",)\n    )\n    cancelled_request_metric = Counter(\n        \"cancelled_request_count\", \"Number of cancelled requests\", namespace=\"daemon_api\",\n        labelnames=(\"method\",)\n    )\n    response_time_metric = Histogram(\n        \"response_time\", \"Response times\", namespace=\"daemon_api\", buckets=HISTOGRAM_BUCKETS,\n        labelnames=(\"method\",)\n    )\n\n    def __init__(self, conf: Config, component_manager: typing.Optional[ComponentManager] = None):\n        self.conf = conf\n        self.platform_info = system_info.get_platform()\n        self._video_file_analyzer = VideoFileAnalyzer(conf)\n        self._node_id = None\n        self._installation_id = None\n        self.session_id = base58.b58encode(utils.generate_id()).decode()\n        self.analytics_manager = analytics.AnalyticsManager(conf, self.installation_id, self.session_id)\n        self.component_manager = component_manager or ComponentManager(\n            conf, analytics_manager=self.analytics_manager,\n            skip_components=conf.components_to_skip or []\n        )\n        self.component_startup_task = None\n\n        logging.getLogger('aiohttp.access').setLevel(logging.WARN)\n        rpc_app = web.Application()\n        rpc_app.router.add_get('/lbryapi', self.handle_old_jsonrpc)\n        rpc_app.router.add_post('/lbryapi', self.handle_old_jsonrpc)\n        rpc_app.router.add_post('/', self.handle_old_jsonrpc)\n        rpc_app.router.add_options('/', self.add_cors_headers)\n        self.rpc_runner = web.AppRunner(rpc_app)\n\n        streaming_app = web.Application()\n        streaming_app.router.add_get('/get/{claim_name}', self.handle_stream_get_request)\n        streaming_app.router.add_get('/get/{claim_name}/{claim_id}', self.handle_stream_get_request)\n        streaming_app.router.add_get('/stream/{sd_hash}', self.handle_stream_range_request)\n        self.streaming_runner = web.AppRunner(streaming_app)\n\n        prom_app = web.Application()\n        prom_app.router.add_get('/metrics', self.handle_metrics_get_request)\n        self.metrics_runner = web.AppRunner(prom_app)\n\n    @property\n    def dht_node(self) -> typing.Optional['Node']:\n        return self.component_manager.get_component(DHT_COMPONENT)\n\n    @property\n    def wallet_manager(self) -> typing.Optional['WalletManager']:\n        return self.component_manager.get_component(WALLET_COMPONENT)\n\n    @property\n    def storage(self) -> typing.Optional['SQLiteStorage']:\n        return self.component_manager.get_component(DATABASE_COMPONENT)\n\n    @property\n    def file_manager(self) -> typing.Optional['FileManager']:\n        return self.component_manager.get_component(FILE_MANAGER_COMPONENT)\n\n    @property\n    def exchange_rate_manager(self) -> typing.Optional['ExchangeRateManager']:\n        return self.component_manager.get_component(EXCHANGE_RATE_MANAGER_COMPONENT)\n\n    @property\n    def blob_manager(self) -> typing.Optional['BlobManager']:\n        return self.component_manager.get_component(BLOB_COMPONENT)\n\n    @property\n    def disk_space_manager(self) -> typing.Optional['DiskSpaceManager']:\n        return self.component_manager.get_component(DISK_SPACE_COMPONENT)\n\n    @property\n    def upnp(self) -> typing.Optional['UPnPComponent']:\n        return self.component_manager.get_component(UPNP_COMPONENT)\n\n    @classmethod\n    def get_api_definitions(cls):\n        prefix = 'jsonrpc_'\n        not_grouped = ['routing_table_get', 'ffmpeg_find']\n        api = {\n            'groups': {\n                group_name[:-len('_DOC')].lower(): getattr(cls, group_name).strip()\n                for group_name in dir(cls) if group_name.endswith('_DOC')\n            },\n            'commands': {}\n        }\n        for jsonrpc_method in dir(cls):\n            if jsonrpc_method.startswith(prefix):\n                full_name = jsonrpc_method[len(prefix):]\n                method = getattr(cls, jsonrpc_method)\n                if full_name in not_grouped:\n                    name_parts = [full_name]\n                else:\n                    name_parts = full_name.split('_', 1)\n                if len(name_parts) == 1:\n                    group = None\n                    name, = name_parts\n                elif len(name_parts) == 2:\n                    group, name = name_parts\n                    assert group in api['groups'], \\\n                        f\"Group {group} does not have doc string for command {full_name}.\"\n                else:\n                    raise NameError(f'Could not parse method name: {jsonrpc_method}')\n                api['commands'][full_name] = {\n                    'api_method_name': full_name,\n                    'name': name,\n                    'group': group,\n                    'doc': method.__doc__,\n                    'method': method,\n                }\n                if hasattr(method, '_deprecated'):\n                    api['commands'][full_name]['replaced_by'] = method.new_command\n\n        for command in api['commands'].values():\n            if 'replaced_by' in command:\n                command['replaced_by'] = api['commands'][command['replaced_by']]\n\n        return api\n\n    @property\n    def db_revision_file_path(self):\n        return os.path.join(self.conf.data_dir, 'db_revision')\n\n    @property\n    def installation_id(self):\n        install_id_filename = os.path.join(self.conf.data_dir, \"install_id\")\n        if not self._installation_id:\n            if os.path.isfile(install_id_filename):\n                with open(install_id_filename, \"r\") as install_id_file:\n                    self._installation_id = str(install_id_file.read()).strip()\n        if not self._installation_id:\n            self._installation_id = base58.b58encode(utils.generate_id()).decode()\n            with open(install_id_filename, \"w\") as install_id_file:\n                install_id_file.write(self._installation_id)\n        return self._installation_id\n\n    def ensure_data_dir(self):\n        if not os.path.isdir(self.conf.data_dir):\n            os.makedirs(self.conf.data_dir)\n        if not os.path.isdir(os.path.join(self.conf.data_dir, \"blobfiles\")):\n            os.makedirs(os.path.join(self.conf.data_dir, \"blobfiles\"))\n        return self.conf.data_dir\n\n    def ensure_wallet_dir(self):\n        if not os.path.isdir(self.conf.wallet_dir):\n            os.makedirs(self.conf.wallet_dir)\n\n    def ensure_download_dir(self):\n        if not os.path.isdir(self.conf.download_dir):\n            os.makedirs(self.conf.download_dir)\n\n    async def start(self):\n        log.info(\"Starting LBRYNet Daemon\")\n        log.debug(\"Settings: %s\", json.dumps(self.conf.settings_dict, indent=2))\n        log.info(\"Platform: %s\", json.dumps(self.platform_info, indent=2))\n\n        await self.analytics_manager.send_server_startup()\n        await self.rpc_runner.setup()\n        await self.streaming_runner.setup()\n        await self.metrics_runner.setup()\n\n        try:\n            rpc_site = web.TCPSite(self.rpc_runner, self.conf.api_host, self.conf.api_port, shutdown_timeout=.5)\n            await rpc_site.start()\n            log.info('RPC server listening on TCP %s:%i', *rpc_site._server.sockets[0].getsockname()[:2])\n        except OSError as e:\n            log.error('RPC server failed to bind TCP %s:%i', self.conf.api_host, self.conf.api_port)\n            await self.analytics_manager.send_server_startup_error(str(e))\n            raise SystemExit()\n\n        try:\n            streaming_site = web.TCPSite(self.streaming_runner, self.conf.streaming_host, self.conf.streaming_port,\n                                         shutdown_timeout=.5)\n            await streaming_site.start()\n            log.info('media server listening on TCP %s:%i', *streaming_site._server.sockets[0].getsockname()[:2])\n\n        except OSError as e:\n            log.error('media server failed to bind TCP %s:%i', self.conf.streaming_host, self.conf.streaming_port)\n            await self.analytics_manager.send_server_startup_error(str(e))\n            raise SystemExit()\n\n        if self.conf.prometheus_port:\n            try:\n                prom_site = web.TCPSite(self.metrics_runner, \"0.0.0.0\", self.conf.prometheus_port, shutdown_timeout=.5)\n                await prom_site.start()\n                log.info('metrics server listening on TCP %s:%i', *prom_site._server.sockets[0].getsockname()[:2])\n            except OSError as e:\n                log.error('metrics server failed to bind TCP :%i', self.conf.prometheus_port)\n                await self.analytics_manager.send_server_startup_error(str(e))\n                raise SystemExit()\n\n        try:\n            await self.initialize()\n        except asyncio.CancelledError:\n            log.info(\"shutting down before finished starting\")\n            await self.analytics_manager.send_server_startup_error(\"shutting down before finished starting\")\n            raise\n        except Exception as e:\n            await self.analytics_manager.send_server_startup_error(str(e))\n            log.exception('Failed to start lbrynet')\n            raise SystemExit()\n\n        await self.analytics_manager.send_server_startup_success()\n\n    async def initialize(self):\n        self.ensure_data_dir()\n        self.ensure_wallet_dir()\n        self.ensure_download_dir()\n        if not self.analytics_manager.is_started:\n            await self.analytics_manager.start()\n        self.component_startup_task = asyncio.create_task(self.component_manager.start())\n        await self.component_startup_task\n\n    async def stop(self):\n        if self.component_startup_task is not None:\n            if self.component_startup_task.done():\n                await self.component_manager.stop()\n            else:\n                self.component_startup_task.cancel()\n                # the wallet component might have not started\n                try:\n                    wallet_component = self.component_manager.get_actual_component('wallet')\n                except NameError:\n                    pass\n                else:\n                    await wallet_component.stop()\n                await self.component_manager.stop()\n        log.info(\"stopped api components\")\n        await self.rpc_runner.cleanup()\n        await self.streaming_runner.cleanup()\n        await self.metrics_runner.cleanup()\n        log.info(\"stopped api server\")\n        if self.analytics_manager.is_started:\n            self.analytics_manager.stop()\n        log.info(\"finished shutting down\")\n\n    async def add_cors_headers(self, request):\n        if self.conf.allowed_origin:\n            return web.Response(\n                headers={\n                    'Access-Control-Allow-Origin': self.conf.allowed_origin,\n                    'Access-Control-Allow-Methods': self.conf.allowed_origin,\n                    'Access-Control-Allow-Headers': self.conf.allowed_origin,\n                }\n            )\n        return None\n\n    async def handle_old_jsonrpc(self, request):\n        ensure_request_allowed(request, self.conf)\n        data = await request.json()\n        params = data.get('params', {})\n        include_protobuf = params.pop('include_protobuf', False) if isinstance(params, dict) else False\n        result = await self._process_rpc_call(data)\n        ledger = None\n        if 'wallet' in self.component_manager.get_components_status():\n            # self.ledger only available if wallet component is not skipped\n            ledger = self.ledger\n        try:\n            encoded_result = jsonrpc_dumps_pretty(\n                result, ledger=ledger, include_protobuf=include_protobuf)\n        except Exception:\n            log.exception('Failed to encode JSON RPC result:')\n            encoded_result = jsonrpc_dumps_pretty(JSONRPCError(\n                JSONRPCError.CODE_APPLICATION_ERROR,\n                'After successfully executing the command, failed to encode result for JSON RPC response.',\n                {'traceback': format_exc()}\n            ), ledger=ledger)\n        headers = {}\n        if self.conf.allowed_origin:\n            headers.update({\n                'Access-Control-Allow-Origin': self.conf.allowed_origin,\n                'Access-Control-Allow-Methods': self.conf.allowed_origin,\n                'Access-Control-Allow-Headers': self.conf.allowed_origin,\n            })\n        return web.Response(\n            text=encoded_result,\n            headers=headers,\n            content_type='application/json'\n        )\n\n    @staticmethod\n    async def handle_metrics_get_request(request: web.Request):\n        try:\n            return web.Response(\n                text=prom_generate_latest().decode(),\n                content_type='text/plain; version=0.0.4'\n            )\n        except Exception:\n            log.exception('could not generate prometheus data')\n            raise\n\n    async def handle_stream_get_request(self, request: web.Request):\n        if not self.conf.streaming_get:\n            log.warning(\"streaming_get is disabled, rejecting request\")\n            raise web.HTTPForbidden()\n        name_and_claim_id = request.path.split(\"/get/\")[1]\n        if \"/\" not in name_and_claim_id:\n            uri = f\"lbry://{name_and_claim_id}\"\n        else:\n            name, claim_id = name_and_claim_id.split(\"/\")\n            uri = f\"lbry://{name}#{claim_id}\"\n        if not self.file_manager.started.is_set():\n            await self.file_manager.started.wait()\n        stream = await self.jsonrpc_get(uri)\n        if isinstance(stream, dict):\n            raise web.HTTPServerError(text=stream['error'])\n        raise web.HTTPFound(f\"/stream/{stream.sd_hash}\")\n\n    async def handle_stream_range_request(self, request: web.Request):\n        try:\n            return await self._handle_stream_range_request(request)\n        except web.HTTPException as err:\n            log.warning(\"http code during /stream range request: %s\", err)\n            raise err\n        except asyncio.CancelledError:\n            # if not excepted here, it would bubble up the error to the console. every time you closed\n            # a running tab, you'd get this error in the console\n            log.debug(\"/stream range request cancelled\")\n        except Exception:\n            log.exception(\"error handling /stream range request\")\n            raise\n        finally:\n            log.debug(\"finished handling /stream range request\")\n\n    async def _handle_stream_range_request(self, request: web.Request):\n        sd_hash = request.path.split(\"/stream/\")[1]\n        if not self.file_manager.started.is_set():\n            await self.file_manager.started.wait()\n        if sd_hash not in self.file_manager.streams:\n            return web.HTTPNotFound()\n        return await self.file_manager.stream_partial_content(request, sd_hash)\n\n    async def _process_rpc_call(self, data):\n        args = data.get('params', {})\n\n        try:\n            function_name = data['method']\n        except KeyError:\n            return JSONRPCError(\n                JSONRPCError.CODE_METHOD_NOT_FOUND,\n                \"Missing 'method' value in request.\"\n            )\n\n        try:\n            method = self._get_jsonrpc_method(function_name)\n        except UnknownAPIMethodError:\n            return JSONRPCError(\n                JSONRPCError.CODE_METHOD_NOT_FOUND,\n                str(CommandDoesNotExistError(function_name))\n            )\n\n        if args in ([{}], []):\n            _args, _kwargs = (), {}\n        elif isinstance(args, dict):\n            _args, _kwargs = (), args\n        elif isinstance(args, list) and len(args) == 1 and isinstance(args[0], dict):\n            # TODO: this is for backwards compatibility. Remove this once API and UI are updated\n            # TODO: also delete EMPTY_PARAMS then\n            _args, _kwargs = (), args[0]\n        elif isinstance(args, list) and len(args) == 2 and \\\n                isinstance(args[0], list) and isinstance(args[1], dict):\n            _args, _kwargs = args\n        else:\n            return JSONRPCError(\n                JSONRPCError.CODE_INVALID_PARAMS,\n                f\"Invalid parameters format: {args}\"\n            )\n\n        if is_transactional_function(function_name):\n            log.info(\"%s %s %s\", function_name, _args, _kwargs)\n\n        params_error, erroneous_params = self._check_params(method, _args, _kwargs)\n        if params_error is not None:\n            params_error_message = '{} for {} command: {}'.format(\n                params_error, function_name, ', '.join(erroneous_params)\n            )\n            log.warning(params_error_message)\n            return JSONRPCError(\n                JSONRPCError.CODE_INVALID_PARAMS,\n                params_error_message,\n            )\n        self.pending_requests_metric.labels(method=function_name).inc()\n        self.requests_count_metric.labels(method=function_name).inc()\n        start = time.perf_counter()\n        try:\n            result = method(self, *_args, **_kwargs)\n            if asyncio.iscoroutine(result):\n                result = await result\n            return result\n        except asyncio.CancelledError:\n            self.cancelled_request_metric.labels(method=function_name).inc()\n            log.info(\"cancelled API call for: %s\", function_name)\n            raise\n        except Exception as e:  # pylint: disable=broad-except\n            self.failed_request_metric.labels(method=function_name).inc()\n            if not isinstance(e, BaseError):\n                log.exception(\"error handling api request\")\n            else:\n                log.error(\"error handling api request: %s\", e)\n            return JSONRPCError.create_command_exception(\n                command=function_name, args=_args, kwargs=_kwargs, exception=e, traceback=format_exc()\n            )\n        finally:\n            self.pending_requests_metric.labels(method=function_name).dec()\n            self.response_time_metric.labels(method=function_name).observe(time.perf_counter() - start)\n\n    def _verify_method_is_callable(self, function_path):\n        if function_path not in self.callable_methods:\n            raise UnknownAPIMethodError(function_path)\n\n    def _get_jsonrpc_method(self, function_path):\n        if function_path in self.deprecated_methods:\n            new_command = self.deprecated_methods[function_path].new_command\n            log.warning('API function \\\"%s\\\" is deprecated, please update to use \\\"%s\\\"',\n                        function_path, new_command)\n            function_path = new_command\n        self._verify_method_is_callable(function_path)\n        return self.callable_methods.get(function_path)\n\n    @staticmethod\n    def _check_params(function, args_tup, args_dict):\n        argspec = inspect.getfullargspec(undecorated(function))\n        num_optional_params = 0 if argspec.defaults is None else len(argspec.defaults)\n\n        duplicate_params = [\n            duplicate_param\n            for duplicate_param in argspec.args[1:len(args_tup) + 1]\n            if duplicate_param in args_dict\n        ]\n\n        if duplicate_params:\n            return 'Duplicate parameters', duplicate_params\n\n        missing_required_params = [\n            required_param\n            for required_param in argspec.args[len(args_tup) + 1:-num_optional_params]\n            if required_param not in args_dict\n        ]\n        if len(missing_required_params) > 0:\n            return 'Missing required parameters', missing_required_params\n\n        extraneous_params = [] if argspec.varkw is not None else [\n            extra_param\n            for extra_param in args_dict\n            if extra_param not in argspec.args[1:]\n        ]\n        if len(extraneous_params) > 0:\n            return 'Extraneous parameters', extraneous_params\n\n        return None, None\n\n    @property\n    def ledger(self) -> Optional['Ledger']:\n        try:\n            return self.wallet_manager.default_account.ledger\n        except AttributeError:\n            return None\n\n    async def get_est_cost_from_uri(self, uri: str) -> typing.Optional[float]:\n        \"\"\"\n        Resolve a name and return the estimated stream cost\n        \"\"\"\n\n        resolved = await self.resolve([], uri)\n        if resolved:\n            claim_response = resolved[uri]\n        else:\n            claim_response = None\n\n        if claim_response and 'claim' in claim_response:\n            if 'value' in claim_response['claim'] and claim_response['claim']['value'] is not None:\n                claim_value = Claim.from_bytes(claim_response['claim']['value'])\n                if not claim_value.stream.has_fee:\n                    return 0.0\n                return round(\n                    self.exchange_rate_manager.convert_currency(\n                        claim_value.stream.fee.currency, \"LBC\", claim_value.stream.fee.amount\n                    ), 5\n                )\n            else:\n                log.warning(\"Failed to estimate cost for %s\", uri)\n\n    ############################################################################\n    #                                                                          #\n    #                JSON-RPC API methods start here                           #\n    #                                                                          #\n    ############################################################################\n\n    def jsonrpc_stop(self):  # pylint: disable=no-self-use\n        \"\"\"\n        Stop lbrynet API server.\n\n        Usage:\n            stop\n\n        Options:\n            None\n\n        Returns:\n            (string) Shutdown message\n        \"\"\"\n\n        def shutdown():\n            raise web.GracefulExit()\n\n        log.info(\"Shutting down lbrynet daemon\")\n        asyncio.get_event_loop().call_later(0, shutdown)\n        return \"Shutting down\"\n\n    async def jsonrpc_ffmpeg_find(self):\n        \"\"\"\n        Get ffmpeg installation information\n\n        Usage:\n            ffmpeg_find\n\n        Options:\n            None\n\n        Returns:\n            (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            }\n        \"\"\"\n        return await self._video_file_analyzer.status(reset=True, recheck=True)\n\n    async def jsonrpc_status(self):\n        \"\"\"\n        Get daemon status\n\n        Usage:\n            status\n\n        Options:\n            None\n\n        Returns:\n            (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                            <source ip and tcp port>: (int) bytes per second received,\n                        },\n                        'outgoing_bps': {\n                            <destination ip and tcp port>: (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                        <TCP | UDP>: (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            }\n        \"\"\"\n        ffmpeg_status = await self._video_file_analyzer.status()\n        running_components = self.component_manager.get_components_status()\n        response = {\n            'installation_id': self.installation_id,\n            'is_running': all(running_components.values()),\n            'skipped_components': self.component_manager.skip_components,\n            'startup_status': running_components,\n            'ffmpeg_status': ffmpeg_status\n        }\n        for component in self.component_manager.components:\n            status = await component.get_status()\n            if status:\n                response[component.component_name] = status\n        return response\n\n    def jsonrpc_version(self):  # pylint: disable=no-self-use\n        \"\"\"\n        Get lbrynet API server version information\n\n        Usage:\n            version\n\n        Options:\n            None\n\n        Returns:\n            (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            }\n        \"\"\"\n        return self.platform_info\n\n    @requires(WALLET_COMPONENT)\n    async def jsonrpc_resolve(self, urls: typing.Union[str, list], wallet_id=None, **kwargs):\n        \"\"\"\n        Get the claim that a URL refers to.\n\n        Usage:\n            resolve <urls>... [--wallet_id=<wallet_id>]\n                    [--include_purchase_receipt]\n                    [--include_is_my_output]\n                    [--include_sent_supports]\n                    [--include_sent_tips]\n                    [--include_received_tips]\n                    [--new_sdk_server=<new_sdk_server>]\n\n        Options:\n            --urls=<urls>              : (str, list) one or more urls to resolve\n            --wallet_id=<wallet_id>    : (str) wallet to check for claim purchase receipts\n           --new_sdk_server=<new_sdk_server> : (str) URL of the new SDK server (EXPERIMENTAL)\n           --include_purchase_receipt  : (bool) lookup and include a receipt if this wallet\n                                                has purchased the claim being resolved\n            --include_is_my_output     : (bool) lookup and include a boolean indicating\n                                                if claim being resolved is yours\n            --include_sent_supports    : (bool) lookup and sum the total amount\n                                                of supports you've made to this claim\n            --include_sent_tips        : (bool) lookup and sum the total amount\n                                                of tips you've made to this claim\n                                                (only makes sense when claim is not yours)\n            --include_received_tips    : (bool) lookup and sum the total amount\n                                                of tips you've received to this claim\n                                                (only makes sense when claim is yours)\n\n        Returns:\n            Dictionary of results, keyed by url\n            '<url>': {\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            }\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n\n        if isinstance(urls, str):\n            urls = [urls]\n\n        results = {}\n\n        valid_urls = set()\n        for url in urls:\n            try:\n                URL.parse(url)\n                valid_urls.add(url)\n            except ValueError:\n                results[url] = {\"error\": f\"{url} is not a valid url\"}\n\n        resolved = await self.resolve(wallet.accounts, list(valid_urls), **kwargs)\n\n        for resolved_uri in resolved:\n            results[resolved_uri] = resolved[resolved_uri] if resolved[resolved_uri] is not None else \\\n                {\"error\": f\"{resolved_uri} did not resolve to a claim\"}\n\n        return results\n\n    @requires(WALLET_COMPONENT, EXCHANGE_RATE_MANAGER_COMPONENT, BLOB_COMPONENT, DATABASE_COMPONENT,\n              FILE_MANAGER_COMPONENT)\n    async def jsonrpc_get(\n            self, uri, file_name=None, download_directory=None, timeout=None, save_file=None, wallet_id=None):\n        \"\"\"\n        Download stream from a LBRY name.\n\n        Usage:\n            get <uri> [<file_name> | --file_name=<file_name>]\n             [<download_directory> | --download_directory=<download_directory>] [<timeout> | --timeout=<timeout>]\n             [--save_file=<save_file>] [--wallet_id=<wallet_id>]\n\n\n        Options:\n            --uri=<uri>              : (str) uri of the content to download\n            --file_name=<file_name>  : (str) specified name for the downloaded file, overrides the stream file name\n            --download_directory=<download_directory>  : (str) full path to the directory to download into\n            --timeout=<timeout>      : (int) download timeout in number of seconds\n            --save_file=<save_file>  : (bool) save the file to the downloads directory\n            --wallet_id=<wallet_id>  : (str) wallet to check for claim purchase receipts\n\n        Returns: {File}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        if download_directory and not os.path.isdir(download_directory):\n            return {\"error\": f\"specified download directory \\\"{download_directory}\\\" does not exist\"}\n        try:\n            stream = await self.file_manager.download_from_uri(\n                uri, self.exchange_rate_manager, timeout, file_name, download_directory,\n                save_file=save_file, wallet=wallet\n            )\n            if not stream:\n                raise DownloadSDTimeoutError(uri)\n        except Exception as e:\n            # TODO: use error from lbry.error\n            log.warning(\"Error downloading %s: %s\", uri, str(e))\n            return {\"error\": str(e)}\n        return stream\n\n    SETTINGS_DOC = \"\"\"\n    Settings management.\n    \"\"\"\n\n    def jsonrpc_settings_get(self):\n        \"\"\"\n        Get daemon settings\n\n        Usage:\n            settings_get\n\n        Options:\n            None\n\n        Returns:\n            (dict) Dictionary of daemon settings\n            See ADJUSTABLE_SETTINGS in lbry/conf.py for full list of settings\n        \"\"\"\n        return self.conf.settings_dict\n\n    def jsonrpc_settings_set(self, key, value):\n        \"\"\"\n        Set daemon settings\n\n        Usage:\n            settings_set (<key>) (<value>)\n\n        Options:\n            None\n\n        Returns:\n            (dict) Updated dictionary of daemon settings\n        \"\"\"\n        with self.conf.update_config() as c:\n            if value and isinstance(value, str) and value[0] in ('[', '{'):\n                value = json.loads(value)\n            attr: Setting = getattr(type(c), key)\n            cleaned = attr.deserialize(value)\n            setattr(c, key, cleaned)\n        return {key: cleaned}\n\n    def jsonrpc_settings_clear(self, key):\n        \"\"\"\n        Clear daemon settings\n\n        Usage:\n            settings_clear (<key>)\n\n        Options:\n            None\n\n        Returns:\n            (dict) Updated dictionary of daemon settings\n        \"\"\"\n        with self.conf.update_config() as c:\n            setattr(c, key, NOT_SET)\n        return {key: self.conf.settings_dict[key]}\n\n    PREFERENCE_DOC = \"\"\"\n    Preferences management.\n    \"\"\"\n\n    def jsonrpc_preference_get(self, key=None, wallet_id=None):\n        \"\"\"\n        Get preference value for key or all values if not key is passed in.\n\n        Usage:\n            preference_get [<key>] [--wallet_id=<wallet_id>]\n\n        Options:\n            --key=<key> : (str) key associated with value\n            --wallet_id=<wallet_id>   : (str) restrict operation to specific wallet\n\n        Returns:\n            (dict) Dictionary of preference(s)\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        if key:\n            if key in wallet.preferences:\n                return {key: wallet.preferences[key]}\n            return\n        return wallet.preferences.to_dict_without_ts()\n\n    def jsonrpc_preference_set(self, key, value, wallet_id=None):\n        \"\"\"\n        Set preferences\n\n        Usage:\n            preference_set (<key>) (<value>) [--wallet_id=<wallet_id>]\n\n        Options:\n            --key=<key> : (str) key associated with value\n            --value=<key> : (str) key associated with value\n            --wallet_id=<wallet_id>   : (str) restrict operation to specific wallet\n\n        Returns:\n            (dict) Dictionary with key/value of new preference\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        if value and isinstance(value, str) and value[0] in ('[', '{'):\n            value = json.loads(value)\n        wallet.preferences[key] = value\n        wallet.save()\n        return {key: value}\n\n    WALLET_DOC = \"\"\"\n    Create, modify and inspect wallets.\n    \"\"\"\n\n    @requires(\"wallet\")\n    def jsonrpc_wallet_list(self, wallet_id=None, page=None, page_size=None):\n        \"\"\"\n        List wallets.\n\n        Usage:\n            wallet_list [--wallet_id=<wallet_id>] [--page=<page>] [--page_size=<page_size>]\n\n        Options:\n            --wallet_id=<wallet_id>  : (str) show specific wallet only\n            --page=<page>            : (int) page to return during paginating\n            --page_size=<page_size>  : (int) number of items on page during pagination\n\n        Returns: {Paginated[Wallet]}\n        \"\"\"\n        if wallet_id:\n            return paginate_list([self.wallet_manager.get_wallet_or_error(wallet_id)], 1, 1)\n        return paginate_list(self.wallet_manager.wallets, page, page_size)\n\n    def jsonrpc_wallet_reconnect(self):\n        \"\"\"\n        Reconnects ledger network client, applying new configurations.\n\n        Usage:\n            wallet_reconnect\n\n        Options:\n\n        Returns: None\n        \"\"\"\n        return self.wallet_manager.reset()\n\n    @requires(\"wallet\")\n    async def jsonrpc_wallet_create(\n            self, wallet_id, skip_on_startup=False, create_account=False, single_key=False):\n        \"\"\"\n        Create a new wallet.\n\n        Usage:\n            wallet_create (<wallet_id> | --wallet_id=<wallet_id>) [--skip_on_startup]\n                          [--create_account] [--single_key]\n\n        Options:\n            --wallet_id=<wallet_id>  : (str) wallet file name\n            --skip_on_startup        : (bool) don't add wallet to daemon_settings.yml\n            --create_account         : (bool) generates the default account\n            --single_key             : (bool) used with --create_account, creates single-key account\n\n        Returns: {Wallet}\n        \"\"\"\n        wallet_path = os.path.join(self.conf.wallet_dir, 'wallets', wallet_id)\n        for wallet in self.wallet_manager.wallets:\n            if wallet.id == wallet_id:\n                raise WalletAlreadyLoadedError(wallet_path)\n        if os.path.exists(wallet_path):\n            raise WalletAlreadyExistsError(wallet_path)\n\n        wallet = self.wallet_manager.import_wallet(wallet_path)\n        if not wallet.accounts and create_account:\n            account = Account.generate(\n                self.ledger, wallet, address_generator={\n                    'name': SingleKey.name if single_key else HierarchicalDeterministic.name\n                }\n            )\n            if self.ledger.network.is_connected:\n                await self.ledger.subscribe_account(account)\n        wallet.save()\n        if not skip_on_startup:\n            with self.conf.update_config() as c:\n                c.wallets += [wallet_id]\n        return wallet\n\n    @requires(\"wallet\")\n    async def jsonrpc_wallet_export(self, password=None, wallet_id=None):\n        \"\"\"\n        Exports encrypted wallet data if password is supplied; otherwise plain JSON.\n\n        Wallet must be unlocked to perform this operation.\n\n        Usage:\n            wallet_export [--password=<password>] [--wallet_id=<wallet_id>]\n\n        Options:\n            --password=<password>         : (str) password to encrypt outgoing data\n            --wallet_id=<wallet_id>       : (str) wallet being exported\n\n        Returns:\n            (str) data: base64-encoded encrypted wallet, or cleartext JSON\n\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        if password is None:\n            return wallet.to_json()\n        return wallet.pack(password).decode()\n\n    @requires(\"wallet\")\n    async def jsonrpc_wallet_import(self, data, password=None, wallet_id=None, blocking=False):\n        \"\"\"\n        Import wallet data and merge accounts and preferences. Data is expected to be JSON if\n        password is not supplied.\n\n        Wallet must be unlocked to perform this operation.\n\n        Usage:\n            wallet_import (<data> | --data=<data>) [<password> | --password=<password>]\n                          [--wallet_id=<wallet_id>] [--blocking]\n\n        Options:\n            --data=<data>                 : (str) incoming wallet data\n            --password=<password>         : (str) password to decrypt incoming data\n            --wallet_id=<wallet_id>       : (str) wallet being merged into\n            --blocking                    : (bool) wait until any new accounts have merged\n\n        Returns:\n            (str) base64-encoded encrypted wallet, or cleartext JSON\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        added_accounts, merged_accounts = wallet.merge(self.wallet_manager, password, data)\n        for new_account in itertools.chain(added_accounts, merged_accounts):\n            await new_account.maybe_migrate_certificates()\n        if added_accounts and self.ledger.network.is_connected:\n            if blocking:\n                await asyncio.wait([\n                    a.ledger.subscribe_account(a) for a in added_accounts\n                ])\n            else:\n                for new_account in added_accounts:\n                    asyncio.create_task(self.ledger.subscribe_account(new_account))\n        wallet.save()\n        return await self.jsonrpc_wallet_export(password=password, wallet_id=wallet_id)\n\n    @requires(\"wallet\")\n    async def jsonrpc_wallet_add(self, wallet_id):\n        \"\"\"\n        Add existing wallet.\n\n        Usage:\n            wallet_add (<wallet_id> | --wallet_id=<wallet_id>)\n\n        Options:\n            --wallet_id=<wallet_id>  : (str) wallet file name\n\n        Returns: {Wallet}\n        \"\"\"\n        wallet_path = os.path.join(self.conf.wallet_dir, 'wallets', wallet_id)\n        for wallet in self.wallet_manager.wallets:\n            if wallet.id == wallet_id:\n                raise WalletAlreadyLoadedError(wallet_path)\n        if not os.path.exists(wallet_path):\n            raise WalletNotFoundError(wallet_path)\n        wallet = self.wallet_manager.import_wallet(wallet_path)\n        if self.ledger.network.is_connected:\n            for account in wallet.accounts:\n                await self.ledger.subscribe_account(account)\n        return wallet\n\n    @requires(\"wallet\")\n    async def jsonrpc_wallet_remove(self, wallet_id):\n        \"\"\"\n        Remove an existing wallet.\n\n        Usage:\n            wallet_remove (<wallet_id> | --wallet_id=<wallet_id>)\n\n        Options:\n            --wallet_id=<wallet_id>    : (str) name of wallet to remove\n\n        Returns: {Wallet}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_error(wallet_id)\n        self.wallet_manager.wallets.remove(wallet)\n        for account in wallet.accounts:\n            await self.ledger.unsubscribe_account(account)\n        return wallet\n\n    @requires(\"wallet\")\n    async def jsonrpc_wallet_balance(self, wallet_id=None, confirmations=0):\n        \"\"\"\n        Return the balance of a wallet\n\n        Usage:\n            wallet_balance [--wallet_id=<wallet_id>] [--confirmations=<confirmations>]\n\n        Options:\n            --wallet_id=<wallet_id>         : (str) balance for specific wallet\n            --confirmations=<confirmations> : (int) Only include transactions with this many\n                                              confirmed blocks.\n\n        Returns:\n            (decimal) amount of lbry credits in wallet\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        balance = await self.ledger.get_detailed_balance(\n            accounts=wallet.accounts, confirmations=confirmations\n        )\n        return dict_values_to_lbc(balance)\n\n    def jsonrpc_wallet_status(self, wallet_id=None):\n        \"\"\"\n        Status of wallet including encryption/lock state.\n\n        Usage:\n            wallet_status [<wallet_id> | --wallet_id=<wallet_id>]\n\n        Options:\n            --wallet_id=<wallet_id>    : (str) status of specific wallet\n\n        Returns:\n            Dictionary of wallet status information.\n        \"\"\"\n        if self.wallet_manager is None:\n            return {'is_encrypted': None, 'is_syncing': None, 'is_locked': None}\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        return {\n            'is_encrypted': wallet.is_encrypted,\n            'is_syncing': len(self.ledger._update_tasks) > 0,\n            'is_locked': wallet.is_locked\n        }\n\n    @requires(WALLET_COMPONENT)\n    def jsonrpc_wallet_unlock(self, password, wallet_id=None):\n        \"\"\"\n        Unlock an encrypted wallet\n\n        Usage:\n            wallet_unlock (<password> | --password=<password>) [--wallet_id=<wallet_id>]\n\n        Options:\n            --password=<password>      : (str) password to use for unlocking\n            --wallet_id=<wallet_id>    : (str) restrict operation to specific wallet\n\n        Returns:\n            (bool) true if wallet is unlocked, otherwise false\n        \"\"\"\n        return self.wallet_manager.get_wallet_or_default(wallet_id).unlock(password)\n\n    @requires(WALLET_COMPONENT)\n    def jsonrpc_wallet_lock(self, wallet_id=None):\n        \"\"\"\n        Lock an unlocked wallet\n\n        Usage:\n            wallet_lock [--wallet_id=<wallet_id>]\n\n        Options:\n            --wallet_id=<wallet_id>    : (str) restrict operation to specific wallet\n\n        Returns:\n            (bool) true if wallet is locked, otherwise false\n        \"\"\"\n        return self.wallet_manager.get_wallet_or_default(wallet_id).lock()\n\n    @requires(WALLET_COMPONENT)\n    def jsonrpc_wallet_decrypt(self, wallet_id=None):\n        \"\"\"\n        Decrypt an encrypted wallet, this will remove the wallet password. The wallet must be unlocked to decrypt it\n\n        Usage:\n            wallet_decrypt [--wallet_id=<wallet_id>]\n\n        Options:\n            --wallet_id=<wallet_id>    : (str) restrict operation to specific wallet\n\n        Returns:\n            (bool) true if wallet is decrypted, otherwise false\n        \"\"\"\n        return self.wallet_manager.get_wallet_or_default(wallet_id).decrypt()\n\n    @requires(WALLET_COMPONENT)\n    def jsonrpc_wallet_encrypt(self, new_password, wallet_id=None):\n        \"\"\"\n        Encrypt an unencrypted wallet with a password\n\n        Usage:\n            wallet_encrypt (<new_password> | --new_password=<new_password>)\n                            [--wallet_id=<wallet_id>]\n\n        Options:\n            --new_password=<new_password>  : (str) password to encrypt account\n            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet\n\n        Returns:\n            (bool) true if wallet is decrypted, otherwise false\n        \"\"\"\n        return self.wallet_manager.get_wallet_or_default(wallet_id).encrypt(new_password)\n\n    @requires(WALLET_COMPONENT)\n    async def jsonrpc_wallet_send(\n            self, amount, addresses, wallet_id=None,\n            change_account_id=None, funding_account_ids=None, preview=False, blocking=True):\n        \"\"\"\n        Send the same number of credits to multiple addresses using all accounts in wallet to\n        fund the transaction and the default account to receive any change.\n\n        Usage:\n            wallet_send <amount> <addresses>... [--wallet_id=<wallet_id>] [--preview]\n                        [--change_account_id=None] [--funding_account_ids=<funding_account_ids>...]\n                        [--blocking]\n\n        Options:\n            --wallet_id=<wallet_id>         : (str) restrict operation to specific wallet\n            --change_account_id=<wallet_id> : (str) account where change will go\n            --funding_account_ids=<funding_account_ids> : (str) accounts to fund the transaction\n            --preview                       : (bool) do not broadcast the transaction\n            --blocking                      : (bool) wait until tx has synced\n\n        Returns: {Transaction}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        assert not wallet.is_locked, \"Cannot spend funds with locked wallet, unlock first.\"\n        account = wallet.get_account_or_default(change_account_id)\n        accounts = wallet.get_accounts_or_all(funding_account_ids)\n\n        amount = self.get_dewies_or_error(\"amount\", amount)\n\n        if addresses and not isinstance(addresses, list):\n            addresses = [addresses]\n\n        outputs = []\n        for address in addresses:\n            self.valid_address_or_error(address, allow_script_address=True)\n            if self.ledger.is_pubkey_address(address):\n                outputs.append(\n                    Output.pay_pubkey_hash(\n                        amount, self.ledger.address_to_hash160(address)\n                    )\n                )\n            elif self.ledger.is_script_address(address):\n                outputs.append(\n                    Output.pay_script_hash(\n                        amount, self.ledger.address_to_hash160(address)\n                    )\n                )\n            else:\n                raise ValueError(f\"Unsupported address: '{address}'\")  # TODO: use error from lbry.error\n\n        tx = await Transaction.create(\n            [], outputs, accounts, account\n        )\n        if not preview:\n            await self.broadcast_or_release(tx, blocking)\n            self.component_manager.loop.create_task(self.analytics_manager.send_credits_sent())\n        else:\n            await self.ledger.release_tx(tx)\n        return tx\n\n    ACCOUNT_DOC = \"\"\"\n    Create, modify and inspect wallet accounts.\n    \"\"\"\n\n    @requires(\"wallet\")\n    async def jsonrpc_account_list(\n            self, account_id=None, wallet_id=None, confirmations=0,\n            include_claims=False, show_seed=False, page=None, page_size=None):\n        \"\"\"\n        List details of all of the accounts or a specific account.\n\n        Usage:\n            account_list [<account_id>] [--wallet_id=<wallet_id>]\n                         [--confirmations=<confirmations>]\n                         [--include_claims] [--show_seed]\n                         [--page=<page>] [--page_size=<page_size>]\n\n        Options:\n            --account_id=<account_id>       : (str) If provided only the balance for this\n                                                    account will be given\n            --wallet_id=<wallet_id>         : (str) accounts in specific wallet\n            --confirmations=<confirmations> : (int) required confirmations (default: 0)\n            --include_claims                : (bool) include claims, requires than a\n                                                     LBC account is specified (default: false)\n            --show_seed                     : (bool) show the seed for the account\n            --page=<page>                   : (int) page to return during paginating\n            --page_size=<page_size>         : (int) number of items on page during pagination\n\n        Returns: {Paginated[Account]}\n        \"\"\"\n        kwargs = {\n            'confirmations': confirmations,\n            'show_seed': show_seed\n        }\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        if account_id:\n            return paginate_list([await wallet.get_account_or_error(account_id).get_details(**kwargs)], 1, 1)\n        else:\n            return paginate_list(await wallet.get_detailed_accounts(**kwargs), page, page_size)\n\n    @requires(\"wallet\")\n    async def jsonrpc_account_balance(self, account_id=None, wallet_id=None, confirmations=0):\n        \"\"\"\n        Return the balance of an account\n\n        Usage:\n            account_balance [<account_id>] [<address> | --address=<address>] [--wallet_id=<wallet_id>]\n                            [<confirmations> | --confirmations=<confirmations>]\n\n        Options:\n            --account_id=<account_id>       : (str) If provided only the balance for this\n                                              account will be given. Otherwise default account.\n            --wallet_id=<wallet_id>         : (str) balance for specific wallet\n            --confirmations=<confirmations> : (int) Only include transactions with this many\n                                              confirmed blocks.\n\n        Returns:\n            (decimal) amount of lbry credits in wallet\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        account = wallet.get_account_or_default(account_id)\n        balance = await account.get_detailed_balance(\n            confirmations=confirmations, read_only=True\n        )\n        return dict_values_to_lbc(balance)\n\n    @requires(\"wallet\")\n    async def jsonrpc_account_add(\n            self, account_name, wallet_id=None, single_key=False,\n            seed=None, private_key=None, public_key=None):\n        \"\"\"\n        Add a previously created account from a seed, private key or public key (read-only).\n        Specify --single_key for single address or vanity address accounts.\n\n        Usage:\n            account_add (<account_name> | --account_name=<account_name>)\n                 (--seed=<seed> | --private_key=<private_key> | --public_key=<public_key>)\n                 [--single_key] [--wallet_id=<wallet_id>]\n\n        Options:\n            --account_name=<account_name>  : (str) name of the account to add\n            --seed=<seed>                  : (str) seed to generate new account from\n            --private_key=<private_key>    : (str) private key for new account\n            --public_key=<public_key>      : (str) public key for new account\n            --single_key                   : (bool) create single key account, default is multi-key\n            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet\n\n        Returns: {Account}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        account = Account.from_dict(\n            self.ledger, wallet, {\n                'name': account_name,\n                'seed': seed,\n                'private_key': private_key,\n                'public_key': public_key,\n                'address_generator': {\n                    'name': SingleKey.name if single_key else HierarchicalDeterministic.name\n                }\n            }\n        )\n        wallet.save()\n        if self.ledger.network.is_connected:\n            await self.ledger.subscribe_account(account)\n        return account\n\n    @requires(\"wallet\")\n    async def jsonrpc_account_create(self, account_name, single_key=False, wallet_id=None):\n        \"\"\"\n        Create a new account. Specify --single_key if you want to use\n        the same address for all transactions (not recommended).\n\n        Usage:\n            account_create (<account_name> | --account_name=<account_name>)\n                           [--single_key] [--wallet_id=<wallet_id>]\n\n        Options:\n            --account_name=<account_name>  : (str) name of the account to create\n            --single_key                   : (bool) create single key account, default is multi-key\n            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet\n\n        Returns: {Account}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        account = Account.generate(\n            self.ledger, wallet, account_name, {\n                'name': SingleKey.name if single_key else HierarchicalDeterministic.name\n            }\n        )\n        wallet.save()\n        if self.ledger.network.is_connected:\n            await self.ledger.subscribe_account(account)\n        return account\n\n    @requires(\"wallet\")\n    def jsonrpc_account_remove(self, account_id, wallet_id=None):\n        \"\"\"\n        Remove an existing account.\n\n        Usage:\n            account_remove (<account_id> | --account_id=<account_id>) [--wallet_id=<wallet_id>]\n\n        Options:\n            --account_id=<account_id>  : (str) id of the account to remove\n            --wallet_id=<wallet_id>    : (str) restrict operation to specific wallet\n\n        Returns: {Account}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        account = wallet.get_account_or_error(account_id)\n        wallet.accounts.remove(account)\n        wallet.save()\n        return account\n\n    @requires(\"wallet\")\n    def jsonrpc_account_set(\n            self, account_id, wallet_id=None, default=False, new_name=None,\n            change_gap=None, change_max_uses=None, receiving_gap=None, receiving_max_uses=None):\n        \"\"\"\n        Change various settings on an account.\n\n        Usage:\n            account_set (<account_id> | --account_id=<account_id>) [--wallet_id=<wallet_id>]\n                [--default] [--new_name=<new_name>]\n                [--change_gap=<change_gap>] [--change_max_uses=<change_max_uses>]\n                [--receiving_gap=<receiving_gap>] [--receiving_max_uses=<receiving_max_uses>]\n\n        Options:\n            --account_id=<account_id>       : (str) id of the account to change\n            --wallet_id=<wallet_id>         : (str) restrict operation to specific wallet\n            --default                       : (bool) make this account the default\n            --new_name=<new_name>           : (str) new name for the account\n            --receiving_gap=<receiving_gap> : (int) set the gap for receiving addresses\n            --receiving_max_uses=<receiving_max_uses> : (int) set the maximum number of times to\n                                                              use a receiving address\n            --change_gap=<change_gap>           : (int) set the gap for change addresses\n            --change_max_uses=<change_max_uses> : (int) set the maximum number of times to\n                                                        use a change address\n\n        Returns: {Account}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        account = wallet.get_account_or_error(account_id)\n        change_made = False\n\n        if account.receiving.name == HierarchicalDeterministic.name:\n            address_changes = {\n                'change': {'gap': change_gap, 'maximum_uses_per_address': change_max_uses},\n                'receiving': {'gap': receiving_gap, 'maximum_uses_per_address': receiving_max_uses},\n            }\n            for chain_name, changes in address_changes.items():\n                chain = getattr(account, chain_name)\n                for attr, value in changes.items():\n                    if value is not None:\n                        setattr(chain, attr, value)\n                        change_made = True\n\n        if new_name is not None:\n            account.name = new_name\n            change_made = True\n\n        if default and wallet.default_account != account:\n            wallet.accounts.remove(account)\n            wallet.accounts.insert(0, account)\n            change_made = True\n\n        if change_made:\n            account.modified_on = int(time.time())\n            wallet.save()\n\n        return account\n\n    @requires(\"wallet\")\n    def jsonrpc_account_max_address_gap(self, account_id, wallet_id=None):\n        \"\"\"\n        Finds ranges of consecutive addresses that are unused and returns the length\n        of the longest such range: for change and receiving address chains. This is\n        useful to figure out ideal values to set for 'receiving_gap' and 'change_gap'\n        account settings.\n\n        Usage:\n            account_max_address_gap (<account_id> | --account_id=<account_id>)\n                                    [--wallet_id=<wallet_id>]\n\n        Options:\n            --account_id=<account_id>  : (str) account for which to get max gaps\n            --wallet_id=<wallet_id>    : (str) restrict operation to specific wallet\n\n        Returns:\n            (map) maximum gap for change and receiving addresses\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        return wallet.get_account_or_error(account_id).get_max_gap()\n\n    @requires(\"wallet\")\n    def jsonrpc_account_fund(self, to_account=None, from_account=None, amount='0.0',\n                             everything=False, outputs=1, broadcast=False, wallet_id=None):\n        \"\"\"\n        Transfer some amount (or --everything) to an account from another\n        account (can be the same account). Amounts are interpreted as LBC.\n        You can also spread the transfer across a number of --outputs (cannot\n        be used together with --everything).\n\n        Usage:\n            account_fund [<to_account> | --to_account=<to_account>]\n                [<from_account> | --from_account=<from_account>]\n                (<amount> | --amount=<amount> | --everything)\n                [<outputs> | --outputs=<outputs>] [--wallet_id=<wallet_id>]\n                [--broadcast]\n\n        Options:\n            --to_account=<to_account>     : (str) send to this account\n            --from_account=<from_account> : (str) spend from this account\n            --amount=<amount>             : (decimal) the amount to transfer lbc\n            --everything                  : (bool) transfer everything (excluding claims), default: false.\n            --outputs=<outputs>           : (int) split payment across many outputs, default: 1.\n            --wallet_id=<wallet_id>       : (str) limit operation to specific wallet.\n            --broadcast                   : (bool) actually broadcast the transaction, default: false.\n\n        Returns: {Transaction}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        to_account = wallet.get_account_or_default(to_account)\n        from_account = wallet.get_account_or_default(from_account)\n        amount = self.get_dewies_or_error('amount', amount) if amount else None\n        if not isinstance(outputs, int):\n            # TODO: use error from lbry.error\n            raise ValueError(\"--outputs must be an integer.\")\n        if everything and outputs > 1:\n            # TODO: use error from lbry.error\n            raise ValueError(\"Using --everything along with --outputs is not supported.\")\n        return from_account.fund(\n            to_account=to_account, amount=amount, everything=everything,\n            outputs=outputs, broadcast=broadcast\n        )\n\n    @requires(\"wallet\")\n    async def jsonrpc_account_deposit(\n        self, txid, nout, redeem_script, private_key,\n        to_account=None, wallet_id=None, preview=False, blocking=False\n    ):\n        \"\"\"\n        Spend a time locked transaction into your account.\n\n        Usage:\n            account_deposit <txid> <nout> <redeem_script> <private_key>\n                [<to_account> | --to_account=<to_account>]\n                [--wallet_id=<wallet_id>] [--preview] [--blocking]\n\n        Options:\n            --txid=<txid>                   : (str) id of the transaction\n            --nout=<nout>                   : (int) output number in the transaction\n            --redeem_script=<redeem_script> : (str) redeem script for output\n            --private_key=<private_key>     : (str) private key to sign transaction\n            --to_account=<to_account>       : (str) deposit to this account\n            --wallet_id=<wallet_id>         : (str) limit operation to specific wallet.\n            --preview                       : (bool) do not broadcast the transaction\n            --blocking                      : (bool) wait until tx has synced\n\n        Returns: {Transaction}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        account = wallet.get_account_or_default(to_account)\n        other_tx = await self.wallet_manager.get_transaction(txid)\n        tx = await Transaction.spend_time_lock(\n            other_tx.outputs[nout], unhexlify(redeem_script), account\n        )\n        pk = PrivateKey.from_bytes(\n            account.ledger, Base58.decode_check(private_key)[1:-1]\n        )\n        await tx.sign([account], {pk.address: pk})\n        if not preview:\n            await self.broadcast_or_release(tx, blocking)\n            self.component_manager.loop.create_task(self.analytics_manager.send_credits_sent())\n        else:\n            await self.ledger.release_tx(tx)\n        return tx\n\n    @requires(WALLET_COMPONENT)\n    def jsonrpc_account_send(self, amount, addresses, account_id=None, wallet_id=None, preview=False, blocking=False):\n        \"\"\"\n        Send the same number of credits to multiple addresses from a specific account (or default account).\n\n        Usage:\n            account_send <amount> <addresses>... [--account_id=<account_id>] [--wallet_id=<wallet_id>] [--preview]\n                                                 [--blocking]\n\n        Options:\n            --account_id=<account_id>  : (str) account to fund the transaction\n            --wallet_id=<wallet_id>    : (str) restrict operation to specific wallet\n            --preview                  : (bool) do not broadcast the transaction\n            --blocking                 : (bool) wait until tx has synced\n\n        Returns: {Transaction}\n        \"\"\"\n        return self.jsonrpc_wallet_send(\n            amount=amount, addresses=addresses, wallet_id=wallet_id,\n            change_account_id=account_id, funding_account_ids=[account_id] if account_id else [],\n            preview=preview, blocking=blocking\n        )\n\n    SYNC_DOC = \"\"\"\n    Wallet synchronization.\n    \"\"\"\n\n    @requires(\"wallet\")\n    def jsonrpc_sync_hash(self, wallet_id=None):\n        \"\"\"\n        Deterministic hash of the wallet.\n\n        Usage:\n            sync_hash [<wallet_id> | --wallet_id=<wallet_id>]\n\n        Options:\n            --wallet_id=<wallet_id>   : (str) wallet for which to generate hash\n\n        Returns:\n            (str) sha256 hash of wallet\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        return hexlify(wallet.hash).decode()\n\n    @requires(\"wallet\")\n    async def jsonrpc_sync_apply(self, password, data=None, wallet_id=None, blocking=False):\n        \"\"\"\n        Apply incoming synchronization data, if provided, and return a sync hash and update wallet data.\n\n        Wallet must be unlocked to perform this operation.\n\n        If \"encrypt-on-disk\" preference is True and supplied password is different from local password,\n        or there is no local password (because local wallet was not encrypted), then the supplied password\n        will be used for local encryption (overwriting previous local encryption password).\n\n        Usage:\n            sync_apply <password> [--data=<data>] [--wallet_id=<wallet_id>] [--blocking]\n\n        Options:\n            --password=<password>         : (str) password to decrypt incoming and encrypt outgoing data\n            --data=<data>                 : (str) incoming sync data, if any\n            --wallet_id=<wallet_id>       : (str) wallet being sync'ed\n            --blocking                    : (bool) wait until any new accounts have sync'ed\n\n        Returns:\n            (map) sync hash and data\n\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        wallet_changed = False\n        if data is not None:\n            added_accounts, merged_accounts = wallet.merge(self.wallet_manager, password, data)\n            for new_account in itertools.chain(added_accounts, merged_accounts):\n                await new_account.maybe_migrate_certificates()\n            if added_accounts and self.ledger.network.is_connected:\n                if blocking:\n                    await asyncio.wait([\n                        a.ledger.subscribe_account(a) for a in added_accounts\n                    ])\n                else:\n                    for new_account in added_accounts:\n                        asyncio.create_task(self.ledger.subscribe_account(new_account))\n            wallet_changed = True\n        if wallet.preferences.get(ENCRYPT_ON_DISK, False) and password != wallet.encryption_password:\n            wallet.encryption_password = password\n            wallet_changed = True\n        if wallet_changed:\n            wallet.save()\n        encrypted = wallet.pack(password)\n        return {\n            'hash': self.jsonrpc_sync_hash(wallet_id),\n            'data': encrypted.decode()\n        }\n\n    ADDRESS_DOC = \"\"\"\n    List, generate and verify addresses.\n    \"\"\"\n\n    @requires(WALLET_COMPONENT)\n    async def jsonrpc_address_is_mine(self, address, account_id=None, wallet_id=None):\n        \"\"\"\n        Checks if an address is associated with the current wallet.\n\n        Usage:\n            address_is_mine (<address> | --address=<address>)\n                            [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>]\n\n        Options:\n            --address=<address>       : (str) address to check\n            --account_id=<account_id> : (str) id of the account to use\n            --wallet_id=<wallet_id>   : (str) restrict operation to specific wallet\n\n        Returns:\n            (bool) true, if address is associated with current wallet\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        account = wallet.get_account_or_default(account_id)\n        match = await self.ledger.db.get_address(read_only=True, address=address, accounts=[account])\n        if match is not None:\n            return True\n        return False\n\n    @requires(WALLET_COMPONENT)\n    def jsonrpc_address_list(self, address=None, account_id=None, wallet_id=None, page=None, page_size=None):\n        \"\"\"\n        List account addresses or details of single address.\n\n        Usage:\n            address_list [--address=<address>] [--account_id=<account_id>] [--wallet_id=<wallet_id>]\n                         [--page=<page>] [--page_size=<page_size>]\n\n        Options:\n            --address=<address>        : (str) just show details for single address\n            --account_id=<account_id>  : (str) id of the account to use\n            --wallet_id=<wallet_id>    : (str) restrict operation to specific wallet\n            --page=<page>              : (int) page to return during paginating\n            --page_size=<page_size>    : (int) number of items on page during pagination\n\n        Returns: {Paginated[Address]}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        constraints = {\n            'cols': ('address', 'account', 'used_times', 'pubkey', 'chain_code', 'n', 'depth')\n        }\n        if address:\n            constraints['address'] = address\n        if account_id:\n            constraints['accounts'] = [wallet.get_account_or_error(account_id)]\n        else:\n            constraints['accounts'] = wallet.accounts\n        return paginate_rows(\n            self.ledger.get_addresses,\n            self.ledger.get_address_count,\n            page, page_size, read_only=True, **constraints\n        )\n\n    @requires(WALLET_COMPONENT)\n    def jsonrpc_address_unused(self, account_id=None, wallet_id=None):\n        \"\"\"\n        Return an address containing no balance, will create\n        a new address if there is none.\n\n        Usage:\n            address_unused [--account_id=<account_id>] [--wallet_id=<wallet_id>]\n\n        Options:\n            --account_id=<account_id> : (str) id of the account to use\n            --wallet_id=<wallet_id>   : (str) restrict operation to specific wallet\n\n        Returns: {Address}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        return wallet.get_account_or_default(account_id).receiving.get_or_create_usable_address()\n\n    FILE_DOC = \"\"\"\n    File management.\n    \"\"\"\n\n    @requires(FILE_MANAGER_COMPONENT)\n    async def jsonrpc_file_list(self, sort=None, reverse=False, comparison=None, wallet_id=None, page=None,\n                                page_size=None, **kwargs):\n        \"\"\"\n        List files limited by optional filters\n\n        Usage:\n            file_list [--sd_hash=<sd_hash>] [--file_name=<file_name>] [--stream_hash=<stream_hash>]\n                      [--rowid=<rowid>] [--added_on=<added_on>] [--claim_id=<claim_id>]\n                      [--outpoint=<outpoint>] [--txid=<txid>] [--nout=<nout>]\n                      [--channel_claim_id=<channel_claim_id>] [--channel_name=<channel_name>]\n                      [--claim_name=<claim_name>] [--blobs_in_stream=<blobs_in_stream>]\n                      [--download_path=<download_path>] [--blobs_remaining=<blobs_remaining>]\n                      [--uploading_to_reflector=<uploading_to_reflector>] [--is_fully_reflected=<is_fully_reflected>]\n                      [--status=<status>] [--completed=<completed>] [--sort=<sort_by>] [--comparison=<comparison>]\n                      [--full_status=<full_status>] [--reverse] [--page=<page>] [--page_size=<page_size>]\n                      [--wallet_id=<wallet_id>]\n\n        Options:\n            --sd_hash=<sd_hash>                    : (str) get file with matching sd hash\n            --file_name=<file_name>                : (str) get file with matching file name in the\n                                                     downloads folder\n            --stream_hash=<stream_hash>            : (str) get file with matching stream hash\n            --rowid=<rowid>                        : (int) get file with matching row id\n            --added_on=<added_on>                  : (int) get file with matching time of insertion\n            --claim_id=<claim_id>                  : (str) get file with matching claim id(s)\n            --outpoint=<outpoint>                  : (str) get file with matching claim outpoint(s)\n            --txid=<txid>                          : (str) get file with matching claim txid\n            --nout=<nout>                          : (int) get file with matching claim nout\n            --channel_claim_id=<channel_claim_id>  : (str) get file with matching channel claim id(s)\n            --channel_name=<channel_name>          : (str) get file with matching channel name\n            --claim_name=<claim_name>              : (str) get file with matching claim name\n            --blobs_in_stream=<blobs_in_stream>    : (int) get file with matching blobs in stream\n            --download_path=<download_path>        : (str) get file with matching download path\n            --uploading_to_reflector=<uploading_to_reflector> : (bool) get files currently uploading to reflector\n            --is_fully_reflected=<is_fully_reflected>         : (bool) get files that have been uploaded to reflector\n            --status=<status>                      : (str) match by status, ( running | finished | stopped )\n            --completed=<completed>                : (bool) match only completed\n            --blobs_remaining=<blobs_remaining>    : (int) amount of remaining blobs to download\n            --sort=<sort_by>                       : (str) field to sort by (one of the above filter fields)\n            --comparison=<comparison>              : (str) logical comparison, (eq | ne | g | ge | l | le | in)\n            --page=<page>                          : (int) page to return during paginating\n            --page_size=<page_size>                : (int) number of items on page during pagination\n            --wallet_id=<wallet_id>                : (str) add purchase receipts from this wallet\n\n        Returns: {Paginated[File]}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        sort = sort or 'rowid'\n        comparison = comparison or 'eq'\n\n        paginated = paginate_list(\n            self.file_manager.get_filtered(sort, reverse, comparison, **kwargs), page, page_size\n        )\n        if paginated['items']:\n            receipts = {\n                txo.purchased_claim_id: txo for txo in\n                await self.ledger.db.get_purchases(\n                    accounts=wallet.accounts,\n                    purchased_claim_id__in=[s.claim_id for s in paginated['items']]\n                )\n            }\n            for stream in paginated['items']:\n                stream.purchase_receipt = receipts.get(stream.claim_id)\n        return paginated\n\n    @requires(FILE_MANAGER_COMPONENT)\n    async def jsonrpc_file_set_status(self, status, **kwargs):\n        \"\"\"\n        Start or stop downloading a file\n\n        Usage:\n            file_set_status (<status> | --status=<status>) [--sd_hash=<sd_hash>]\n                      [--file_name=<file_name>] [--stream_hash=<stream_hash>] [--rowid=<rowid>]\n\n        Options:\n            --status=<status>            : (str) one of \"start\" or \"stop\"\n            --sd_hash=<sd_hash>          : (str) set status of file with matching sd hash\n            --file_name=<file_name>      : (str) set status of file with matching file name in the\n                                           downloads folder\n            --stream_hash=<stream_hash>  : (str) set status of file with matching stream hash\n            --rowid=<rowid>              : (int) set status of file with matching row id\n\n        Returns:\n            (str) Confirmation message\n        \"\"\"\n\n        if status not in ['start', 'stop']:\n            # TODO: use error from lbry.error\n            raise Exception('Status must be \"start\" or \"stop\".')\n\n        streams = self.file_manager.get_filtered(**kwargs)\n        if not streams:\n            # TODO: use error from lbry.error\n            raise Exception(f'Unable to find a file for {kwargs}')\n        stream = streams[0]\n        if status == 'start' and not stream.running:\n            if not hasattr(stream, 'bt_infohash') and 'dht' not in self.conf.components_to_skip:\n                stream.downloader.node = self.dht_node\n            await stream.save_file()\n            msg = \"Resumed download\"\n        elif status == 'stop' and stream.running:\n            await stream.stop()\n            msg = \"Stopped download\"\n        else:\n            msg = (\n                \"File was already being downloaded\" if status == 'start'\n                else \"File was already stopped\"\n            )\n        return msg\n\n    @requires(FILE_MANAGER_COMPONENT)\n    async def jsonrpc_file_delete(self, delete_from_download_dir=False, delete_all=False, **kwargs):\n        \"\"\"\n        Delete a LBRY file\n\n        Usage:\n            file_delete [--delete_from_download_dir] [--delete_all] [--sd_hash=<sd_hash>] [--file_name=<file_name>]\n                        [--stream_hash=<stream_hash>] [--rowid=<rowid>] [--claim_id=<claim_id>] [--txid=<txid>]\n                        [--nout=<nout>] [--claim_name=<claim_name>] [--channel_claim_id=<channel_claim_id>]\n                        [--channel_name=<channel_name>]\n\n        Options:\n            --delete_from_download_dir             : (bool) delete file from download directory,\n                                                    instead of just deleting blobs\n            --delete_all                           : (bool) if there are multiple matching files,\n                                                     allow the deletion of multiple files.\n                                                     Otherwise do not delete anything.\n            --sd_hash=<sd_hash>                    : (str) delete by file sd hash\n            --file_name=<file_name>                 : (str) delete by file name in downloads folder\n            --stream_hash=<stream_hash>            : (str) delete by file stream hash\n            --rowid=<rowid>                        : (int) delete by file row id\n            --claim_id=<claim_id>                  : (str) delete by file claim id\n            --txid=<txid>                          : (str) delete by file claim txid\n            --nout=<nout>                          : (int) delete by file claim nout\n            --claim_name=<claim_name>              : (str) delete by file claim name\n            --channel_claim_id=<channel_claim_id>  : (str) delete by file channel claim id\n            --channel_name=<channel_name>                 : (str) delete by file channel claim name\n\n        Returns:\n            (bool) true if deletion was successful\n        \"\"\"\n\n        streams = self.file_manager.get_filtered(**kwargs)\n\n        if len(streams) > 1:\n            if not delete_all:\n                log.warning(\"There are %i files to delete, use narrower filters to select one\",\n                            len(streams))\n                return False\n            else:\n                log.warning(\"Deleting %i files\",\n                            len(streams))\n\n        if not streams:\n            log.warning(\"There is no file to delete\")\n            return False\n        else:\n            for stream in streams:\n                message = f\"Deleted file {stream.file_name}\"\n                await self.file_manager.delete(stream, delete_file=delete_from_download_dir)\n                log.info(message)\n            result = True\n        return result\n\n    @requires(FILE_MANAGER_COMPONENT)\n    async def jsonrpc_file_save(self, file_name=None, download_directory=None, **kwargs):\n        \"\"\"\n        Start saving a file to disk.\n\n        Usage:\n            file_save [--file_name=<file_name>] [--download_directory=<download_directory>] [--sd_hash=<sd_hash>]\n                      [--stream_hash=<stream_hash>] [--rowid=<rowid>] [--claim_id=<claim_id>] [--txid=<txid>]\n                      [--nout=<nout>] [--claim_name=<claim_name>] [--channel_claim_id=<channel_claim_id>]\n                      [--channel_name=<channel_name>]\n\n        Options:\n            --file_name=<file_name>                      : (str) file name to save to\n            --download_directory=<download_directory>    : (str) directory to save into\n            --sd_hash=<sd_hash>                          : (str) save file with matching sd hash\n            --stream_hash=<stream_hash>                  : (str) save file with matching stream hash\n            --rowid=<rowid>                              : (int) save file with matching row id\n            --claim_id=<claim_id>                        : (str) save file with matching claim id\n            --txid=<txid>                                : (str) save file with matching claim txid\n            --nout=<nout>                                : (int) save file with matching claim nout\n            --claim_name=<claim_name>                    : (str) save file with matching claim name\n            --channel_claim_id=<channel_claim_id>        : (str) save file with matching channel claim id\n            --channel_name=<channel_name>                : (str) save file with matching channel claim name\n\n        Returns: {File}\n        \"\"\"\n\n        streams = self.file_manager.get_filtered(**kwargs)\n\n        if len(streams) > 1:\n            log.warning(\"There are %i matching files, use narrower filters to select one\", len(streams))\n            return False\n        if not streams:\n            log.warning(\"There is no file to save\")\n            return False\n        stream = streams[0]\n        if not hasattr(stream, 'bt_infohash') and 'dht' not in self.conf.components_to_skip:\n            stream.downloader.node = self.dht_node\n        await stream.save_file(file_name, download_directory)\n        return stream\n\n    PURCHASE_DOC = \"\"\"\n    List and make purchases of claims.\n    \"\"\"\n\n    @requires(WALLET_COMPONENT)\n    def jsonrpc_purchase_list(\n            self, claim_id=None, resolve=False, account_id=None, wallet_id=None, page=None, page_size=None):\n        \"\"\"\n        List my claim purchases.\n\n        Usage:\n            purchase_list [<claim_id> | --claim_id=<claim_id>] [--resolve]\n                          [--account_id=<account_id>] [--wallet_id=<wallet_id>]\n                          [--page=<page>] [--page_size=<page_size>]\n\n        Options:\n            --claim_id=<claim_id>      : (str) purchases for specific claim\n            --resolve                  : (str) include resolved claim information\n            --account_id=<account_id>  : (str) id of the account to query\n            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet\n            --page=<page>              : (int) page to return during paginating\n            --page_size=<page_size>    : (int) number of items on page during pagination\n\n        Returns: {Paginated[Output]}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        constraints = {\n            \"wallet\": wallet,\n            \"accounts\": [wallet.get_account_or_error(account_id)] if account_id else wallet.accounts,\n            \"resolve\": resolve,\n        }\n        if claim_id:\n            constraints[\"purchased_claim_id\"] = claim_id\n        return paginate_rows(\n            self.ledger.get_purchases,\n            self.ledger.get_purchase_count,\n            page, page_size, **constraints\n        )\n\n    @requires(WALLET_COMPONENT)\n    async def jsonrpc_purchase_create(\n            self, claim_id=None, url=None, wallet_id=None, funding_account_ids=None,\n            allow_duplicate_purchase=False, override_max_key_fee=False, preview=False, blocking=False):\n        \"\"\"\n        Purchase a claim.\n\n        Usage:\n            purchase_create (--claim_id=<claim_id> | --url=<url>) [--wallet_id=<wallet_id>]\n                    [--funding_account_ids=<funding_account_ids>...]\n                    [--allow_duplicate_purchase] [--override_max_key_fee] [--preview] [--blocking]\n\n        Options:\n            --claim_id=<claim_id>          : (str) claim id of claim to purchase\n            --url=<url>                    : (str) lookup claim to purchase by url\n            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet\n          --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction\n            --allow_duplicate_purchase     : (bool) allow purchasing claim_id you already own\n            --override_max_key_fee         : (bool) ignore max key fee for this purchase\n            --preview                      : (bool) do not broadcast the transaction\n            --blocking                     : (bool) wait until transaction is in mempool\n\n        Returns: {Transaction}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        assert not wallet.is_locked, \"Cannot spend funds with locked wallet, unlock first.\"\n        accounts = wallet.get_accounts_or_all(funding_account_ids)\n        txo = None\n        if claim_id:\n            txo = await self.ledger.get_claim_by_claim_id(claim_id, accounts, include_purchase_receipt=True)\n            if not isinstance(txo, Output) or not txo.is_claim:\n                # TODO: use error from lbry.error\n                raise Exception(f\"Could not find claim with claim_id '{claim_id}'.\")\n        elif url:\n            txo = (await self.ledger.resolve(accounts, [url], include_purchase_receipt=True))[url]\n            if not isinstance(txo, Output) or not txo.is_claim:\n                # TODO: use error from lbry.error\n                raise Exception(f\"Could not find claim with url '{url}'.\")\n        else:\n            # TODO: use error from lbry.error\n            raise Exception(\"Missing argument claim_id or url.\")\n        if not allow_duplicate_purchase and txo.purchase_receipt:\n            raise AlreadyPurchasedError(claim_id)\n        claim = txo.claim\n        if not claim.is_stream or not claim.stream.has_fee:\n            # TODO: use error from lbry.error\n            raise Exception(f\"Claim '{claim_id}' does not have a purchase price.\")\n        tx = await self.wallet_manager.create_purchase_transaction(\n            accounts, txo, self.exchange_rate_manager, override_max_key_fee\n        )\n        if not preview:\n            await self.broadcast_or_release(tx, blocking)\n        else:\n            await self.ledger.release_tx(tx)\n        return tx\n\n    CLAIM_DOC = \"\"\"\n    List and search all types of claims.\n    \"\"\"\n\n    @requires(WALLET_COMPONENT)\n    def jsonrpc_claim_list(self, claim_type=None, **kwargs):\n        \"\"\"\n        List my stream and channel claims.\n\n        Usage:\n            claim_list [--claim_type=<claim_type>...] [--claim_id=<claim_id>...] [--name=<name>...] [--is_spent]\n                       [--reposted_claim_id=<reposted_claim_id>...]\n                       [--channel_id=<channel_id>...] [--account_id=<account_id>] [--wallet_id=<wallet_id>]\n                       [--has_source | --has_no_source] [--page=<page>] [--page_size=<page_size>]\n                       [--resolve] [--order_by=<order_by>] [--no_totals] [--include_received_tips]\n\n        Options:\n            --claim_type=<claim_type>  : (str or list) claim type: channel, stream, repost, collection\n            --claim_id=<claim_id>      : (str or list) claim id\n            --channel_id=<channel_id>  : (str or list) streams in this channel\n            --name=<name>              : (str or list) claim name\n            --is_spent                 : (bool) shows previous claim updates and abandons\n            --reposted_claim_id=<reposted_claim_id> : (str or list) reposted claim id\n            --account_id=<account_id>  : (str) id of the account to query\n            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet\n            --has_source               : (bool) list claims containing a source field\n            --has_no_source            : (bool) list claims not containing a source field\n            --page=<page>              : (int) page to return during paginating\n            --page_size=<page_size>    : (int) number of items on page during pagination\n            --resolve                  : (bool) resolves each claim to provide additional metadata\n            --order_by=<order_by>      : (str) field to order by: 'name', 'height', 'amount'\n            --no_totals                : (bool) do not calculate the total number of pages and items in result set\n                                                (significant performance boost)\n            --include_received_tips    : (bool) calculate the amount of tips received for claim outputs\n\n        Returns: {Paginated[Output]}\n        \"\"\"\n        kwargs['type'] = claim_type or CLAIM_TYPE_NAMES\n        if not kwargs.get('is_spent', False):\n            kwargs['is_not_spent'] = True\n        return self.jsonrpc_txo_list(**kwargs)\n\n    async def jsonrpc_support_sum(self, claim_id, new_sdk_server, include_channel_content=False, **kwargs):\n        \"\"\"\n        List total staked supports for a claim, grouped by the channel that signed the support.\n\n        If claim_id is a channel claim, you can use --include_channel_content to also include supports for\n        content claims in the channel.\n\n        !!!! NOTE: PAGINATION DOES NOT DO ANYTHING AT THE MOMENT !!!!!\n\n        Usage:\n            support_sum <claim_id> <new_sdk_server>\n                         [--include_channel_content]\n                         [--page=<page>] [--page_size=<page_size>]\n\n        Options:\n            --claim_id=<claim_id>             : (str)  claim id\n            --new_sdk_server=<new_sdk_server> : (str)  URL of the new SDK server (EXPERIMENTAL)\n            --include_channel_content         : (bool) if claim_id is for a channel, include supports for claims in\n                                                       that channel\n            --page=<page>                     : (int)  page to return during paginating\n            --page_size=<page_size>           : (int)  number of items on page during pagination\n\n        Returns: {Paginated[Dict]}\n        \"\"\"\n        page_num, page_size = abs(kwargs.pop('page', 1)), min(abs(kwargs.pop('page_size', DEFAULT_PAGE_SIZE)), 50)\n        kwargs.update({'offset': page_size * (page_num - 1), 'limit': page_size})\n        support_sums = await self.ledger.sum_supports(\n            new_sdk_server, claim_id=claim_id, include_channel_content=include_channel_content, **kwargs\n        )\n        return {\n            \"items\": support_sums,\n            \"page\": page_num,\n            \"page_size\": page_size\n        }\n\n    @requires(WALLET_COMPONENT)\n    async def jsonrpc_claim_search(self, **kwargs):\n        \"\"\"\n        Search for stream and channel claims on the blockchain.\n\n        Arguments marked with \"supports equality constraints\" allow prepending the\n        value with an equality constraint such as '>', '>=', '<' and '<='\n        eg. --height=\">400000\" would limit results to only claims above 400k block height.\n\n        They also support multiple constraints passed as a list of the args described above.\n        eg. --release_time=[\">1000000\", \"<2000000\"]\n\n        Usage:\n            claim_search [<name> | --name=<name>] [--text=<text>] [--txid=<txid>] [--nout=<nout>]\n                         [--claim_id=<claim_id> | --claim_ids=<claim_ids>...]\n                         [--channel=<channel> |\n                             [[--channel_ids=<channel_ids>...] [--not_channel_ids=<not_channel_ids>...]]]\n                         [--has_channel_signature] [--valid_channel_signature | --invalid_channel_signature]\n                         [--limit_claims_per_channel=<limit_claims_per_channel>]\n                         [--is_controlling] [--release_time=<release_time>] [--public_key_id=<public_key_id>]\n                         [--timestamp=<timestamp>] [--creation_timestamp=<creation_timestamp>]\n                         [--height=<height>] [--creation_height=<creation_height>]\n                         [--activation_height=<activation_height>] [--expiration_height=<expiration_height>]\n                         [--amount=<amount>] [--effective_amount=<effective_amount>]\n                         [--support_amount=<support_amount>] [--trending_group=<trending_group>]\n                         [--trending_mixed=<trending_mixed>] [--trending_local=<trending_local>]\n                         [--trending_global=<trending_global] [--trending_score=<trending_score]\n                         [--reposted_claim_id=<reposted_claim_id>] [--reposted=<reposted>]\n                         [--claim_type=<claim_type>] [--stream_types=<stream_types>...] [--media_types=<media_types>...]\n                         [--fee_currency=<fee_currency>] [--fee_amount=<fee_amount>]\n                         [--duration=<duration>]\n                         [--any_tags=<any_tags>...] [--all_tags=<all_tags>...] [--not_tags=<not_tags>...]\n                         [--any_languages=<any_languages>...] [--all_languages=<all_languages>...]\n                         [--not_languages=<not_languages>...]\n                         [--any_locations=<any_locations>...] [--all_locations=<all_locations>...]\n                         [--not_locations=<not_locations>...]\n                         [--order_by=<order_by>...] [--no_totals] [--page=<page>] [--page_size=<page_size>]\n                         [--wallet_id=<wallet_id>] [--include_purchase_receipt] [--include_is_my_output]\n                         [--remove_duplicates] [--has_source | --has_no_source] [--sd_hash=<sd_hash>]\n                         [--new_sdk_server=<new_sdk_server>]\n\n        Options:\n            --name=<name>                   : (str) claim name (normalized)\n            --text=<text>                   : (str) full text search\n            --claim_id=<claim_id>           : (str) full or partial claim id\n            --claim_ids=<claim_ids>         : (list) list of full claim ids\n            --txid=<txid>                   : (str) transaction id\n            --nout=<nout>                   : (str) position in the transaction\n            --channel=<channel>             : (str) claims signed by this channel (argument is\n                                                    a URL which automatically gets resolved),\n                                                    see --channel_ids if you need to filter by\n                                                    multiple channels at the same time,\n                                                    includes claims with invalid signatures,\n                                                    use in conjunction with --valid_channel_signature\n            --channel_ids=<channel_ids>     : (list) claims signed by any of these channels\n                                                    (arguments must be claim ids of the channels),\n                                                    includes claims with invalid signatures,\n                                                    implies --has_channel_signature,\n                                                    use in conjunction with --valid_channel_signature\n            --not_channel_ids=<not_channel_ids>: (list) exclude claims signed by any of these channels\n                                                    (arguments must be claim ids of the channels)\n            --has_channel_signature         : (bool) claims with a channel signature (valid or invalid)\n            --valid_channel_signature       : (bool) claims with a valid channel signature or no signature,\n                                                     use in conjunction with --has_channel_signature to\n                                                     only get claims with valid signatures\n            --invalid_channel_signature     : (bool) claims with invalid channel signature or no signature,\n                                                     use in conjunction with --has_channel_signature to\n                                                     only get claims with invalid signatures\n            --limit_claims_per_channel=<limit_claims_per_channel>: (int) only return up to the specified\n                                                                         number of claims per channel\n            --is_controlling                : (bool) winning claims of their respective name\n            --public_key_id=<public_key_id> : (str) only return channels having this public key id, this is\n                                                    the same key as used in the wallet file to map\n                                                    channel certificate private keys: {'public_key_id': 'private key'}\n            --height=<height>               : (int) last updated block height (supports equality constraints)\n            --timestamp=<timestamp>         : (int) last updated timestamp (supports equality constraints)\n            --creation_height=<creation_height>      : (int) created at block height (supports equality constraints)\n            --creation_timestamp=<creation_timestamp>: (int) created at timestamp (supports equality constraints)\n            --activation_height=<activation_height>  : (int) height at which claim starts competing for name\n                                                             (supports equality constraints)\n            --expiration_height=<expiration_height>  : (int) height at which claim will expire\n                                                             (supports equality constraints)\n            --release_time=<release_time>   : (int) limit to claims self-described as having been\n                                                    released to the public on or after this UTC\n                                                    timestamp, when claim does not provide\n                                                    a release time the publish time is used instead\n                                                    (supports equality constraints)\n            --amount=<amount>               : (int) limit by claim value (supports equality constraints)\n            --support_amount=<support_amount>: (int) limit by supports and tips received (supports\n                                                    equality constraints)\n            --effective_amount=<effective_amount>: (int) limit by total value (initial claim value plus\n                                                     all tips and supports received), this amount is\n                                                     blank until claim has reached activation height\n                                                     (supports equality constraints)\n            --trending_score=<trending_score>: (int) limit by trending score (supports equality constraints)\n            --trending_group=<trending_group>: (int) DEPRECATED - instead please use trending_score\n            --trending_mixed=<trending_mixed>: (int) DEPRECATED - instead please use trending_score\n            --trending_local=<trending_local>: (int) DEPRECATED - instead please use trending_score\n            --trending_global=<trending_global>: (int) DEPRECATED - instead please use trending_score\n            --reposted_claim_id=<reposted_claim_id>: (str) all reposts of the specified original claim id\n            --reposted=<reposted>           : (int) claims reposted this many times (supports\n                                                    equality constraints)\n            --claim_type=<claim_type>       : (str) filter by 'channel', 'stream', 'repost' or 'collection'\n            --stream_types=<stream_types>   : (list) filter by 'video', 'image', 'document', etc\n            --media_types=<media_types>     : (list) filter by 'video/mp4', 'image/png', etc\n            --fee_currency=<fee_currency>   : (string) specify fee currency: LBC, BTC, USD\n            --fee_amount=<fee_amount>       : (decimal) content download fee (supports equality constraints)\n            --duration=<duration>           : (int) duration of video or audio in seconds\n                                                     (supports equality constraints)\n            --any_tags=<any_tags>           : (list) find claims containing any of the tags\n            --all_tags=<all_tags>           : (list) find claims containing every tag\n            --not_tags=<not_tags>           : (list) find claims not containing any of these tags\n            --any_languages=<any_languages> : (list) find claims containing any of the languages\n            --all_languages=<all_languages> : (list) find claims containing every language\n            --not_languages=<not_languages> : (list) find claims not containing any of these languages\n            --any_locations=<any_locations> : (list) find claims containing any of the locations\n            --all_locations=<all_locations> : (list) find claims containing every location\n            --not_locations=<not_locations> : (list) find claims not containing any of these locations\n            --page=<page>                   : (int) page to return during paginating\n            --page_size=<page_size>         : (int) number of items on page during pagination\n            --order_by=<order_by>           : (list) field to order by, default is descending order, to do an\n                                                    ascending order prepend ^ to the field name, eg. '^amount'\n                                                    available fields: 'name', 'height', 'release_time',\n                                                    'publish_time', 'amount', 'effective_amount',\n                                                    'support_amount', 'trending_group', 'trending_mixed',\n                                                    'trending_local', 'trending_global', 'activation_height'\n            --no_totals                     : (bool) do not calculate the total number of pages and items in result set\n                                                     (significant performance boost)\n            --wallet_id=<wallet_id>         : (str) wallet to check for claim purchase receipts\n            --include_purchase_receipt      : (bool) lookup and include a receipt if this wallet\n                                                     has purchased the claim\n            --include_is_my_output          : (bool) lookup and include a boolean indicating\n                                                     if claim being resolved is yours\n            --remove_duplicates             : (bool) removes duplicated content from search by picking either the\n                                                     original claim or the oldest matching repost\n            --has_source                    : (bool) find claims containing a source field\n            --sd_hash=<sd_hash>             : (str)  find claims where the source stream descriptor hash matches\n                                                     (partially or completely) the given hexadecimal string\n            --has_no_source                 : (bool) find claims not containing a source field\n           --new_sdk_server=<new_sdk_server> : (str) URL of the new SDK server (EXPERIMENTAL)\n\n        Returns: {Paginated[Output]}\n        \"\"\"\n        if \"claim_ids\" in kwargs and not kwargs[\"claim_ids\"]:\n            kwargs.pop(\"claim_ids\")\n        if {'claim_id', 'claim_ids'}.issubset(kwargs):\n            raise ConflictingInputValueError('claim_id', 'claim_ids')\n        if kwargs.pop('valid_channel_signature', False):\n            kwargs['signature_valid'] = 1\n        if kwargs.pop('invalid_channel_signature', False):\n            kwargs['signature_valid'] = 0\n        if 'has_no_source' in kwargs:\n            kwargs['has_source'] = not kwargs.pop('has_no_source')\n        if 'order_by' in kwargs:  # TODO: remove this after removing support for old trending args from the api\n            value = kwargs.pop('order_by')\n            value = value if isinstance(value, list) else [value]\n            new_value = []\n            for new_v in value:\n                migrated = new_v if new_v not in (\n                    'trending_mixed', 'trending_local', 'trending_global', 'trending_group'\n                ) else 'trending_score'\n                if migrated not in new_value:\n                    new_value.append(migrated)\n            kwargs['order_by'] = new_value\n        page_num, page_size = abs(kwargs.pop('page', 1)), min(abs(kwargs.pop('page_size', DEFAULT_PAGE_SIZE)), 50)\n        wallet = self.wallet_manager.get_wallet_or_default(kwargs.pop('wallet_id', None))\n        kwargs.update({'offset': page_size * (page_num - 1), 'limit': page_size})\n        txos, blocked, _, total = await self.ledger.claim_search(wallet.accounts, **kwargs)\n        result = {\n            \"items\": txos,\n            \"blocked\": blocked,\n            \"page\": page_num,\n            \"page_size\": page_size\n        }\n        if not kwargs.pop('no_totals', False):\n            result['total_pages'] = int((total + (page_size - 1)) / page_size)\n            result['total_items'] = total\n        return result\n\n    CHANNEL_DOC = \"\"\"\n    Create, update, abandon and list your channel claims.\n    \"\"\"\n\n    @deprecated('channel_create')\n    def jsonrpc_channel_new(self):\n        \"\"\" deprecated \"\"\"\n\n    @requires(WALLET_COMPONENT)\n    async def jsonrpc_channel_create(\n            self, name, bid, allow_duplicate_name=False, account_id=None, wallet_id=None,\n            claim_address=None, funding_account_ids=None, preview=False, blocking=False, **kwargs):\n        \"\"\"\n        Create a new channel by generating a channel private key and establishing an '@' prefixed claim.\n\n        Usage:\n            channel_create (<name> | --name=<name>) (<bid> | --bid=<bid>)\n                           [--allow_duplicate_name=<allow_duplicate_name>]\n                           [--title=<title>] [--description=<description>] [--email=<email>]\n                           [--website_url=<website_url>] [--featured=<featured>...]\n                           [--tags=<tags>...] [--languages=<languages>...] [--locations=<locations>...]\n                           [--thumbnail_url=<thumbnail_url>] [--cover_url=<cover_url>]\n                           [--account_id=<account_id>] [--wallet_id=<wallet_id>]\n                           [--claim_address=<claim_address>] [--funding_account_ids=<funding_account_ids>...]\n                           [--preview] [--blocking]\n\n        Options:\n            --name=<name>                  : (str) name of the channel prefixed with '@'\n            --bid=<bid>                    : (decimal) amount to back the claim\n        --allow_duplicate_name=<allow_duplicate_name> : (bool) create new channel even if one already exists with\n                                              given name. default: false.\n            --title=<title>                : (str) title of the publication\n            --description=<description>    : (str) description of the publication\n            --email=<email>                : (str) email of channel owner\n            --website_url=<website_url>    : (str) website url\n            --featured=<featured>          : (list) claim_ids of featured content in channel\n            --tags=<tags>                  : (list) content tags\n            --languages=<languages>        : (list) languages used by the channel,\n                                                    using RFC 5646 format, eg:\n                                                    for English `--languages=en`\n                                                    for Spanish (Spain) `--languages=es-ES`\n                                                    for Spanish (Mexican) `--languages=es-MX`\n                                                    for Chinese (Simplified) `--languages=zh-Hans`\n                                                    for Chinese (Traditional) `--languages=zh-Hant`\n            --locations=<locations>        : (list) locations of the channel, consisting of 2 letter\n                                                    `country` code and a `state`, `city` and a postal\n                                                    `code` along with a `latitude` and `longitude`.\n                                                    for JSON RPC: pass a dictionary with aforementioned\n                                                        attributes as keys, eg:\n                                                        ...\n                                                        \"locations\": [{'country': 'US', 'state': 'NH'}]\n                                                        ...\n                                                    for command line: pass a colon delimited list\n                                                        with values in the following order:\n\n                                                          \"COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE\"\n\n                                                        making sure to include colon for blank values, for\n                                                        example to provide only the city:\n\n                                                          ... --locations=\"::Manchester\"\n\n                                                        with all values set:\n\n                                                 ... --locations=\"US:NH:Manchester:03101:42.990605:-71.460989\"\n\n                                                        optionally, you can just pass the \"LATITUDE:LONGITUDE\":\n\n                                                          ... --locations=\"42.990605:-71.460989\"\n\n                                                        finally, you can also pass JSON string of dictionary\n                                                        on the command line as you would via JSON RPC\n\n                                                          ... --locations=\"{'country': 'US', 'state': 'NH'}\"\n\n            --thumbnail_url=<thumbnail_url>: (str) thumbnail url\n            --cover_url=<cover_url>        : (str) url of cover image\n            --account_id=<account_id>      : (str) account to use for holding the transaction\n            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet\n          --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction\n            --claim_address=<claim_address>: (str) address where the channel is sent to, if not specified\n                                                   it will be determined automatically from the account\n            --preview                      : (bool) do not broadcast the transaction\n            --blocking                     : (bool) wait until transaction is in mempool\n\n        Returns: {Transaction}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        assert not wallet.is_locked, \"Cannot spend funds with locked wallet, unlock first.\"\n        account = wallet.get_account_or_default(account_id)\n        funding_accounts = wallet.get_accounts_or_all(funding_account_ids)\n        self.valid_channel_name_or_error(name)\n        amount = self.get_dewies_or_error('bid', bid, positive_value=True)\n        claim_address = await self.get_receiving_address(claim_address, account)\n\n        existing_channels = await self.ledger.get_channels(accounts=wallet.accounts, claim_name=name)\n        if len(existing_channels) > 0:\n            if not allow_duplicate_name:\n                # TODO: use error from lbry.error\n                raise Exception(\n                    f\"You already have a channel under the name '{name}'. \"\n                    f\"Use --allow-duplicate-name flag to override.\"\n                )\n\n        claim = Claim()\n        claim.channel.update(**kwargs)\n        tx = await Transaction.claim_create(\n            name, claim, amount, claim_address, funding_accounts, funding_accounts[0]\n        )\n        txo = tx.outputs[0]\n        txo.set_channel_private_key(\n            await funding_accounts[0].generate_channel_private_key()\n        )\n\n        await tx.sign(funding_accounts)\n\n        if not preview:\n            wallet.save()\n            await self.broadcast_or_release(tx, blocking)\n            self.component_manager.loop.create_task(self.storage.save_claims([self._old_get_temp_claim_info(\n                tx, txo, claim_address, claim, name\n            )]))\n            self.component_manager.loop.create_task(self.analytics_manager.send_new_channel())\n        else:\n            await account.ledger.release_tx(tx)\n\n        return tx\n\n    @requires(WALLET_COMPONENT)\n    async def jsonrpc_channel_update(\n            self, claim_id, bid=None, account_id=None, wallet_id=None, claim_address=None,\n            funding_account_ids=None, new_signing_key=False, preview=False,\n            blocking=False, replace=False, **kwargs):\n        \"\"\"\n        Update an existing channel claim.\n\n        Usage:\n            channel_update (<claim_id> | --claim_id=<claim_id>) [<bid> | --bid=<bid>]\n                           [--title=<title>] [--description=<description>] [--email=<email>]\n                           [--website_url=<website_url>]\n                           [--featured=<featured>...] [--clear_featured]\n                           [--tags=<tags>...] [--clear_tags]\n                           [--languages=<languages>...] [--clear_languages]\n                           [--locations=<locations>...] [--clear_locations]\n                           [--thumbnail_url=<thumbnail_url>] [--cover_url=<cover_url>]\n                           [--account_id=<account_id>] [--wallet_id=<wallet_id>]\n                           [--claim_address=<claim_address>] [--new_signing_key]\n                           [--funding_account_ids=<funding_account_ids>...]\n                           [--preview] [--blocking] [--replace]\n\n        Options:\n            --claim_id=<claim_id>          : (str) claim_id of the channel to update\n            --bid=<bid>                    : (decimal) amount to back the claim\n            --title=<title>                : (str) title of the publication\n            --description=<description>    : (str) description of the publication\n            --email=<email>                : (str) email of channel owner\n            --website_url=<website_url>    : (str) website url\n            --featured=<featured>          : (list) claim_ids of featured content in channel\n            --clear_featured               : (bool) clear existing featured content (prior to adding new ones)\n            --tags=<tags>                  : (list) add content tags\n            --clear_tags                   : (bool) clear existing tags (prior to adding new ones)\n            --languages=<languages>        : (list) languages used by the channel,\n                                                    using RFC 5646 format, eg:\n                                                    for English `--languages=en`\n                                                    for Spanish (Spain) `--languages=es-ES`\n                                                    for Spanish (Mexican) `--languages=es-MX`\n                                                    for Chinese (Simplified) `--languages=zh-Hans`\n                                                    for Chinese (Traditional) `--languages=zh-Hant`\n            --clear_languages              : (bool) clear existing languages (prior to adding new ones)\n            --locations=<locations>        : (list) locations of the channel, consisting of 2 letter\n                                                    `country` code and a `state`, `city` and a postal\n                                                    `code` along with a `latitude` and `longitude`.\n                                                    for JSON RPC: pass a dictionary with aforementioned\n                                                        attributes as keys, eg:\n                                                        ...\n                                                        \"locations\": [{'country': 'US', 'state': 'NH'}]\n                                                        ...\n                                                    for command line: pass a colon delimited list\n                                                        with values in the following order:\n\n                                                          \"COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE\"\n\n                                                        making sure to include colon for blank values, for\n                                                        example to provide only the city:\n\n                                                          ... --locations=\"::Manchester\"\n\n                                                        with all values set:\n\n                                                 ... --locations=\"US:NH:Manchester:03101:42.990605:-71.460989\"\n\n                                                        optionally, you can just pass the \"LATITUDE:LONGITUDE\":\n\n                                                          ... --locations=\"42.990605:-71.460989\"\n\n                                                        finally, you can also pass JSON string of dictionary\n                                                        on the command line as you would via JSON RPC\n\n                                                          ... --locations=\"{'country': 'US', 'state': 'NH'}\"\n\n            --clear_locations              : (bool) clear existing locations (prior to adding new ones)\n            --thumbnail_url=<thumbnail_url>: (str) thumbnail url\n            --cover_url=<cover_url>        : (str) url of cover image\n            --account_id=<account_id>      : (str) account in which to look for channel (default: all)\n            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet\n          --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction\n            --claim_address=<claim_address>: (str) address where the channel is sent\n            --new_signing_key              : (bool) generate a new signing key, will invalidate all previous publishes\n            --preview                      : (bool) do not broadcast the transaction\n            --blocking                     : (bool) wait until transaction is in mempool\n            --replace                      : (bool) instead of modifying specific values on\n                                                    the channel, this will clear all existing values\n                                                    and only save passed in values, useful for form\n                                                    submissions where all values are always set\n\n        Returns: {Transaction}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        assert not wallet.is_locked, \"Cannot spend funds with locked wallet, unlock first.\"\n        funding_accounts = wallet.get_accounts_or_all(funding_account_ids)\n        if account_id:\n            account = wallet.get_account_or_error(account_id)\n            accounts = [account]\n        else:\n            account = wallet.default_account\n            accounts = wallet.accounts\n\n        existing_channels = await self.ledger.get_claims(\n            wallet=wallet, accounts=accounts, claim_id=claim_id\n        )\n        if len(existing_channels) != 1:\n            account_ids = ', '.join(f\"'{account.id}'\" for account in accounts)\n            # TODO: use error from lbry.error\n            raise Exception(\n                f\"Can't find the channel '{claim_id}' in account(s) {account_ids}.\"\n            )\n        old_txo = existing_channels[0]\n        if not old_txo.claim.is_channel:\n            # TODO: use error from lbry.error\n            raise Exception(\n                f\"A claim with id '{claim_id}' was found but it is not a channel.\"\n            )\n\n        if bid is not None:\n            amount = self.get_dewies_or_error('bid', bid, positive_value=True)\n        else:\n            amount = old_txo.amount\n\n        if claim_address is not None:\n            self.valid_address_or_error(claim_address)\n        else:\n            claim_address = old_txo.get_address(account.ledger)\n\n        if replace:\n            claim = Claim()\n            claim.channel.public_key_bytes = old_txo.claim.channel.public_key_bytes\n        else:\n            claim = Claim.from_bytes(old_txo.claim.to_bytes())\n        claim.channel.update(**kwargs)\n        tx = await Transaction.claim_update(\n            old_txo, claim, amount, claim_address, funding_accounts, funding_accounts[0]\n        )\n        new_txo = tx.outputs[0]\n\n        if new_signing_key:\n            new_txo.set_channel_private_key(\n                await funding_accounts[0].generate_channel_private_key()\n            )\n        else:\n            new_txo.private_key = old_txo.private_key\n\n        new_txo.script.generate()\n\n        await tx.sign(funding_accounts)\n\n        if not preview:\n            wallet.save()\n            await self.broadcast_or_release(tx, blocking)\n            self.component_manager.loop.create_task(self.storage.save_claims([self._old_get_temp_claim_info(\n                tx, new_txo, claim_address, new_txo.claim, new_txo.claim_name\n            )]))\n            self.component_manager.loop.create_task(self.analytics_manager.send_new_channel())\n        else:\n            await account.ledger.release_tx(tx)\n\n        return tx\n\n    @requires(WALLET_COMPONENT)\n    async def jsonrpc_channel_sign(\n            self, channel_name=None, channel_id=None, hexdata=None, salt=None,\n            channel_account_id=None, wallet_id=None):\n        \"\"\"\n        Signs data using the specified channel signing key.\n\n        Usage:\n            channel_sign [<channel_name> | --channel_name=<channel_name>] [<channel_id> | --channel_id=<channel_id>]\n                         [<hexdata> | --hexdata=<hexdata>] [<salt> | --salt=<salt>]\n                         [--channel_account_id=<channel_account_id>...] [--wallet_id=<wallet_id>]\n\n        Options:\n            --channel_name=<channel_name>            : (str) name of channel used to sign (or use channel id)\n            --channel_id=<channel_id>                : (str) claim id of channel used to sign (or use channel name)\n            --hexdata=<hexdata>                      : (str) data to sign, encoded as hexadecimal\n            --salt=<salt>                            : (str) salt to use for signing, default is to use timestamp\n            --channel_account_id=<channel_account_id>: (str) one or more account ids for accounts to look in\n                                                             for channel certificates, defaults to all accounts.\n            --wallet_id=<wallet_id>                  : (str) restrict operation to specific wallet\n\n        Returns:\n            (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            }\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        assert not wallet.is_locked, \"Cannot spend funds with locked wallet, unlock first.\"\n        signing_channel = await self.get_channel_or_error(\n            wallet, channel_account_id, channel_id, channel_name, for_signing=True\n        )\n        if salt is None:\n            salt = str(int(time.time()))\n        signature = signing_channel.sign_data(unhexlify(str(hexdata)), salt)\n        return {\n            'signature': signature,\n            'signing_ts': salt,  # DEPRECATED\n            'salt': salt,\n        }\n\n    @requires(WALLET_COMPONENT)\n    async def jsonrpc_channel_abandon(\n            self, claim_id=None, txid=None, nout=None, account_id=None, wallet_id=None,\n            preview=False, blocking=True):\n        \"\"\"\n        Abandon one of my channel claims.\n\n        Usage:\n            channel_abandon [<claim_id> | --claim_id=<claim_id>]\n                            [<txid> | --txid=<txid>] [<nout> | --nout=<nout>]\n                            [--account_id=<account_id>] [--wallet_id=<wallet_id>]\n                            [--preview] [--blocking]\n\n        Options:\n            --claim_id=<claim_id>     : (str) claim_id of the claim to abandon\n            --txid=<txid>             : (str) txid of the claim to abandon\n            --nout=<nout>             : (int) nout of the claim to abandon\n            --account_id=<account_id> : (str) id of the account to use\n            --wallet_id=<wallet_id>   : (str) restrict operation to specific wallet\n            --preview                 : (bool) do not broadcast the transaction\n            --blocking                : (bool) wait until abandon is in mempool\n\n        Returns: {Transaction}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        assert not wallet.is_locked, \"Cannot spend funds with locked wallet, unlock first.\"\n        if account_id:\n            account = wallet.get_account_or_error(account_id)\n            accounts = [account]\n        else:\n            account = wallet.default_account\n            accounts = wallet.accounts\n\n        if txid is not None and nout is not None:\n            claims = await self.ledger.get_claims(\n                wallet=wallet, accounts=accounts, **{'txo.txid': txid, 'txo.position': nout}\n            )\n        elif claim_id is not None:\n            claims = await self.ledger.get_claims(\n                wallet=wallet, accounts=accounts, claim_id=claim_id\n            )\n        else:\n            # TODO: use error from lbry.error\n            raise Exception('Must specify claim_id, or txid and nout')\n\n        if not claims:\n            # TODO: use error from lbry.error\n            raise Exception('No claim found for the specified claim_id or txid:nout')\n\n        tx = await Transaction.create(\n            [Input.spend(txo) for txo in claims], [], [account], account\n        )\n\n        if not preview:\n            await self.broadcast_or_release(tx, blocking)\n            self.component_manager.loop.create_task(self.analytics_manager.send_claim_action('abandon'))\n        else:\n            await account.ledger.release_tx(tx)\n\n        return tx\n\n    @requires(WALLET_COMPONENT)\n    def jsonrpc_channel_list(self, *args, **kwargs):\n        \"\"\"\n        List my channel claims.\n\n        Usage:\n            channel_list [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>]\n                         [--name=<name>...] [--claim_id=<claim_id>...] [--is_spent]\n                         [--page=<page>] [--page_size=<page_size>] [--resolve] [--no_totals]\n\n        Options:\n            --name=<name>              : (str or list) channel name\n            --claim_id=<claim_id>      : (str or list) channel id\n            --is_spent                 : (bool) shows previous channel updates and abandons\n            --account_id=<account_id>  : (str) id of the account to use\n            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet\n            --page=<page>              : (int) page to return during paginating\n            --page_size=<page_size>    : (int) number of items on page during pagination\n            --resolve                  : (bool) resolves each channel to provide additional metadata\n            --no_totals                : (bool) do not calculate the total number of pages and items in result set\n                                                (significant performance boost)\n\n        Returns: {Paginated[Output]}\n        \"\"\"\n        kwargs['type'] = 'channel'\n        if 'is_spent' not in kwargs or not kwargs['is_spent']:\n            kwargs['is_not_spent'] = True\n        return self.jsonrpc_txo_list(*args, **kwargs)\n\n    @requires(WALLET_COMPONENT)\n    async def jsonrpc_channel_export(self, channel_id=None, channel_name=None, account_id=None, wallet_id=None):\n        \"\"\"\n        Export channel private key.\n\n        Usage:\n            channel_export (<channel_id> | --channel_id=<channel_id> | --channel_name=<channel_name>)\n                           [--account_id=<account_id>...] [--wallet_id=<wallet_id>]\n\n        Options:\n            --channel_id=<channel_id>     : (str) claim id of channel to export\n            --channel_name=<channel_name> : (str) name of channel to export\n            --account_id=<account_id>     : (str) one or more account ids for accounts\n                                                  to look in for channels, defaults to\n                                                  all accounts.\n            --wallet_id=<wallet_id>       : (str) restrict operation to specific wallet\n\n        Returns:\n            (str) serialized channel private key\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        channel = await self.get_channel_or_error(wallet, account_id, channel_id, channel_name, for_signing=True)\n        address = channel.get_address(self.ledger)\n        public_key = await self.ledger.get_public_key_for_address(wallet, address)\n        if not public_key:\n            # TODO: use error from lbry.error\n            raise Exception(\"Can't find public key for address holding the channel.\")\n        export = {\n            'name': channel.claim_name,\n            'channel_id': channel.claim_id,\n            'holding_address': address,\n            'holding_public_key': public_key.extended_key_string(),\n            'signing_private_key': channel.private_key.signing_key.to_pem().decode()\n        }\n        return base58.b58encode(json.dumps(export, separators=(',', ':')))\n\n    @requires(WALLET_COMPONENT)\n    async def jsonrpc_channel_import(self, channel_data, wallet_id=None):\n        \"\"\"\n        Import serialized channel private key (to allow signing new streams to the channel)\n\n        Usage:\n            channel_import (<channel_data> | --channel_data=<channel_data>) [--wallet_id=<wallet_id>]\n\n        Options:\n            --channel_data=<channel_data> : (str) serialized channel, as exported by channel export\n            --wallet_id=<wallet_id>       : (str) import into specific wallet\n\n        Returns:\n            (dict) Result dictionary\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n\n        decoded = base58.b58decode(channel_data)\n        data = json.loads(decoded)\n        channel_private_key = PrivateKey.from_pem(\n            self.ledger, data['signing_private_key']\n        )\n\n        # check that the holding_address hasn't changed since the export was made\n        holding_address = data['holding_address']\n        channels, _, _, _ = await self.ledger.claim_search(\n            wallet.accounts, public_key_id=channel_private_key.address\n        )\n        if channels and channels[0].get_address(self.ledger) != holding_address:\n            holding_address = channels[0].get_address(self.ledger)\n\n        account = await self.ledger.get_account_for_address(wallet, holding_address)\n        if account:\n            # Case 1: channel holding address is in one of the accounts we already have\n            #         simply add the certificate to existing account\n            pass\n        else:\n            # Case 2: channel holding address hasn't changed and thus is in the bundled read-only account\n            #         create a single-address holding account to manage the channel\n            if holding_address == data['holding_address']:\n                account = Account.from_dict(self.ledger, wallet, {\n                    'name': f\"Holding Account For Channel {data['name']}\",\n                    'public_key': data['holding_public_key'],\n                    'address_generator': {'name': 'single-address'}\n                })\n                if self.ledger.network.is_connected:\n                    await self.ledger.subscribe_account(account)\n                    await self.ledger._update_tasks.done.wait()\n            # Case 3: the holding address has changed and we can't create or find an account for it\n            else:\n                # TODO: use error from lbry.error\n                raise Exception(\n                    \"Channel owning account has changed since the channel was exported and \"\n                    \"it is not an account to which you have access.\"\n                )\n        account.add_channel_private_key(channel_private_key)\n        wallet.save()\n        return f\"Added channel signing key for {data['name']}.\"\n\n    STREAM_DOC = \"\"\"\n    Create, update, abandon, list and inspect your stream claims.\n    \"\"\"\n\n    @requires(WALLET_COMPONENT, FILE_MANAGER_COMPONENT, BLOB_COMPONENT, DATABASE_COMPONENT)\n    async def jsonrpc_publish(self, name, **kwargs):\n        \"\"\"\n        Create or replace a stream claim at a given name (use 'stream create/update' for more control).\n\n        Usage:\n            publish (<name> | --name=<name>) [--bid=<bid>] [--file_path=<file_path>]\n                    [--file_name=<file_name>] [--file_hash=<file_hash>] [--validate_file] [--optimize_file]\n                    [--fee_currency=<fee_currency>] [--fee_amount=<fee_amount>] [--fee_address=<fee_address>]\n                    [--title=<title>] [--description=<description>] [--author=<author>]\n                    [--tags=<tags>...] [--languages=<languages>...] [--locations=<locations>...]\n                    [--license=<license>] [--license_url=<license_url>] [--thumbnail_url=<thumbnail_url>]\n                    [--release_time=<release_time>] [--width=<width>] [--height=<height>] [--duration=<duration>]\n                    [--sd_hash=<sd_hash>] [--channel_id=<channel_id> | --channel_name=<channel_name>]\n                    [--channel_account_id=<channel_account_id>...]\n                    [--account_id=<account_id>] [--wallet_id=<wallet_id>]\n                    [--claim_address=<claim_address>] [--funding_account_ids=<funding_account_ids>...]\n                    [--preview] [--blocking]\n\n        Options:\n            --name=<name>                  : (str) name of the content (can only consist of a-z A-Z 0-9 and -(dash))\n            --bid=<bid>                    : (decimal) amount to back the claim\n            --file_path=<file_path>        : (str) path to file to be associated with name.\n            --file_name=<file_name>        : (str) name of file to be associated with stream.\n            --file_hash=<file_hash>        : (str) hash of file to be associated with stream.\n            --validate_file                : (bool) validate that the video container and encodings match\n                                             common web browser support or that optimization succeeds if specified.\n                                             FFmpeg is required\n            --optimize_file                : (bool) transcode the video & audio if necessary to ensure\n                                             common web browser support. FFmpeg is required\n            --fee_currency=<fee_currency>  : (string) specify fee currency\n            --fee_amount=<fee_amount>      : (decimal) content download fee\n            --fee_address=<fee_address>    : (str) address where to send fee payments, will use\n                                                   value from --claim_address if not provided\n            --title=<title>                : (str) title of the publication\n            --description=<description>    : (str) description of the publication\n            --author=<author>              : (str) author of the publication. The usage for this field is not\n                                             the same as for channels. The author field is used to credit an author\n                                             who is not the publisher and is not represented by the channel. For\n                                             example, a pdf file of 'The Odyssey' has an author of 'Homer' but may\n                                             by published to a channel such as '@classics', or to no channel at all\n            --tags=<tags>                  : (list) add content tags\n            --languages=<languages>        : (list) languages used by the channel,\n                                                    using RFC 5646 format, eg:\n                                                    for English `--languages=en`\n                                                    for Spanish (Spain) `--languages=es-ES`\n                                                    for Spanish (Mexican) `--languages=es-MX`\n                                                    for Chinese (Simplified) `--languages=zh-Hans`\n                                                    for Chinese (Traditional) `--languages=zh-Hant`\n            --locations=<locations>        : (list) locations relevant to the stream, consisting of 2 letter\n                                                    `country` code and a `state`, `city` and a postal\n                                                    `code` along with a `latitude` and `longitude`.\n                                                    for JSON RPC: pass a dictionary with aforementioned\n                                                        attributes as keys, eg:\n                                                        ...\n                                                        \"locations\": [{'country': 'US', 'state': 'NH'}]\n                                                        ...\n                                                    for command line: pass a colon delimited list\n                                                        with values in the following order:\n\n                                                          \"COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE\"\n\n                                                        making sure to include colon for blank values, for\n                                                        example to provide only the city:\n\n                                                          ... --locations=\"::Manchester\"\n\n                                                        with all values set:\n\n                                                 ... --locations=\"US:NH:Manchester:03101:42.990605:-71.460989\"\n\n                                                        optionally, you can just pass the \"LATITUDE:LONGITUDE\":\n\n                                                          ... --locations=\"42.990605:-71.460989\"\n\n                                                        finally, you can also pass JSON string of dictionary\n                                                        on the command line as you would via JSON RPC\n\n                                                          ... --locations=\"{'country': 'US', 'state': 'NH'}\"\n\n            --license=<license>            : (str) publication license\n            --license_url=<license_url>    : (str) publication license url\n            --thumbnail_url=<thumbnail_url>: (str) thumbnail url\n            --release_time=<release_time>  : (int) original public release of content, seconds since UNIX epoch\n            --width=<width>                : (int) image/video width, automatically calculated from media file\n            --height=<height>              : (int) image/video height, automatically calculated from media file\n            --duration=<duration>          : (int) audio/video duration in seconds, automatically calculated\n            --sd_hash=<sd_hash>            : (str) sd_hash of stream\n            --channel_id=<channel_id>      : (str) claim id of the publisher channel\n            --channel_name=<channel_name>  : (str) name of publisher channel\n          --channel_account_id=<channel_account_id>: (str) one or more account ids for accounts to look in\n                                                   for channel certificates, defaults to all accounts.\n            --account_id=<account_id>      : (str) account to use for holding the transaction\n            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet\n          --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction\n            --claim_address=<claim_address>: (str) address where the claim is sent to, if not specified\n                                                   it will be determined automatically from the account\n            --preview                      : (bool) do not broadcast the transaction\n            --blocking                     : (bool) wait until transaction is in mempool\n\n        Returns: {Transaction}\n        \"\"\"\n        self.valid_stream_name_or_error(name)\n        wallet = self.wallet_manager.get_wallet_or_default(kwargs.get('wallet_id'))\n        if kwargs.get('account_id'):\n            accounts = [wallet.get_account_or_error(kwargs.get('account_id'))]\n        else:\n            accounts = wallet.accounts\n        claims = await self.ledger.get_claims(\n            wallet=wallet, accounts=accounts, claim_name=name\n        )\n        if len(claims) == 0:\n            if 'bid' not in kwargs:\n                # TODO: use error from lbry.error\n                raise Exception(\"'bid' is a required argument for new publishes.\")\n            return await self.jsonrpc_stream_create(name, **kwargs)\n        elif len(claims) == 1:\n            assert claims[0].claim.is_stream, f\"Claim at name '{name}' is not a stream claim.\"\n            return await self.jsonrpc_stream_update(claims[0].claim_id, replace=True, **kwargs)\n        # TODO: use error from lbry.error\n        raise Exception(\n            f\"There are {len(claims)} claims for '{name}', please use 'stream update' command \"\n            f\"to update a specific stream claim.\"\n        )\n\n    @requires(WALLET_COMPONENT, FILE_MANAGER_COMPONENT, BLOB_COMPONENT, DATABASE_COMPONENT)\n    async def jsonrpc_stream_repost(\n            self, name, bid, claim_id, allow_duplicate_name=False, channel_id=None,\n            channel_name=None, channel_account_id=None, account_id=None, wallet_id=None,\n            claim_address=None, funding_account_ids=None, preview=False, blocking=False, **kwargs):\n        \"\"\"\n            Creates a claim that references an existing stream by its claim id.\n\n            Usage:\n                stream_repost (<name> | --name=<name>) (<bid> | --bid=<bid>) (<claim_id> | --claim_id=<claim_id>)\n                        [--allow_duplicate_name=<allow_duplicate_name>]\n                        [--title=<title>] [--description=<description>] [--tags=<tags>...]\n                        [--channel_id=<channel_id> | --channel_name=<channel_name>]\n                        [--channel_account_id=<channel_account_id>...]\n                        [--account_id=<account_id>] [--wallet_id=<wallet_id>]\n                        [--claim_address=<claim_address>] [--funding_account_ids=<funding_account_ids>...]\n                        [--preview] [--blocking]\n\n            Options:\n                --name=<name>                  : (str) name of the content (can only consist of a-z A-Z 0-9 and -(dash))\n                --bid=<bid>                    : (decimal) amount to back the claim\n                --claim_id=<claim_id>          : (str) id of the claim being reposted\n                --allow_duplicate_name=<allow_duplicate_name> : (bool) create new claim even if one already exists with\n                                                                       given name. default: false.\n                --title=<title>                : (str) title of the repost\n                --description=<description>    : (str) description of the repost\n                --tags=<tags>                  : (list) add repost tags\n                --channel_id=<channel_id>      : (str) claim id of the publisher channel\n                --channel_name=<channel_name>  : (str) name of the publisher channel\n                --channel_account_id=<channel_account_id>: (str) one or more account ids for accounts to look in\n                                                                 for channel certificates, defaults to all accounts.\n                --account_id=<account_id>      : (str) account to use for holding the transaction\n                --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet\n                --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction\n                --claim_address=<claim_address>: (str) address where the claim is sent to, if not specified\n                                                       it will be determined automatically from the account\n                --preview                      : (bool) do not broadcast the transaction\n                --blocking                     : (bool) wait until transaction is in mempool\n\n            Returns: {Transaction}\n            \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        self.valid_stream_name_or_error(name)\n        account = wallet.get_account_or_default(account_id)\n        funding_accounts = wallet.get_accounts_or_all(funding_account_ids)\n        channel = await self.get_channel_or_none(wallet, channel_account_id, channel_id, channel_name, for_signing=True)\n        amount = self.get_dewies_or_error('bid', bid, positive_value=True)\n        claim_address = await self.get_receiving_address(claim_address, account)\n        claims = await account.get_claims(claim_name=name)\n        if len(claims) > 0:\n            if not allow_duplicate_name:\n                # TODO: use error from lbry.error\n                raise Exception(\n                    f\"You already have a stream claim published under the name '{name}'. \"\n                    f\"Use --allow-duplicate-name flag to override.\"\n                )\n        if not VALID_FULL_CLAIM_ID.fullmatch(claim_id):\n            # TODO: use error from lbry.error\n            raise Exception('Invalid claim id. It is expected to be a 40 characters long hexadecimal string.')\n\n        claim = Claim()\n        claim.repost.update(**kwargs)\n        claim.repost.reference.claim_id = claim_id\n        tx = await Transaction.claim_create(\n            name, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel\n        )\n        new_txo = tx.outputs[0]\n\n        if channel:\n            new_txo.sign(channel)\n        await tx.sign(funding_accounts)\n\n        if not preview:\n            await self.broadcast_or_release(tx, blocking)\n            self.component_manager.loop.create_task(self.analytics_manager.send_claim_action('publish'))\n        else:\n            await account.ledger.release_tx(tx)\n\n        return tx\n\n    @requires(WALLET_COMPONENT, FILE_MANAGER_COMPONENT, BLOB_COMPONENT, DATABASE_COMPONENT)\n    async def jsonrpc_stream_create(\n            self, name, bid, file_path=None, allow_duplicate_name=False,\n            channel_id=None, channel_name=None, channel_account_id=None,\n            account_id=None, wallet_id=None, claim_address=None, funding_account_ids=None,\n            preview=False, blocking=False, validate_file=False, optimize_file=False, **kwargs):\n        \"\"\"\n        Make a new stream claim and announce the associated file to lbrynet.\n\n        Usage:\n            stream_create (<name> | --name=<name>) (<bid> | --bid=<bid>) [<file_path> | --file_path=<file_path>]\n                    [--file_name=<file_name>] [--file_hash=<file_hash>] [--validate_file] [--optimize_file]\n                    [--allow_duplicate_name=<allow_duplicate_name>]\n                    [--fee_currency=<fee_currency>] [--fee_amount=<fee_amount>] [--fee_address=<fee_address>]\n                    [--title=<title>] [--description=<description>] [--author=<author>]\n                    [--tags=<tags>...] [--languages=<languages>...] [--locations=<locations>...]\n                    [--license=<license>] [--license_url=<license_url>] [--thumbnail_url=<thumbnail_url>]\n                    [--release_time=<release_time>] [--width=<width>] [--height=<height>] [--duration=<duration>]\n                    [--sd_hash=<sd_hash>] [--channel_id=<channel_id> | --channel_name=<channel_name>]\n                    [--channel_account_id=<channel_account_id>...]\n                    [--account_id=<account_id>] [--wallet_id=<wallet_id>]\n                    [--claim_address=<claim_address>] [--funding_account_ids=<funding_account_ids>...]\n                    [--preview] [--blocking]\n\n        Options:\n            --name=<name>                  : (str) name of the content (can only consist of a-z A-Z 0-9 and -(dash))\n            --bid=<bid>                    : (decimal) amount to back the claim\n            --file_path=<file_path>        : (str) path to file to be associated with name.\n            --file_name=<file_name>        : (str) name of file to be associated with stream.\n            --file_hash=<file_hash>        : (str) hash of file to be associated with stream.\n            --validate_file                : (bool) validate that the video container and encodings match\n                                             common web browser support or that optimization succeeds if specified.\n                                             FFmpeg is required\n            --optimize_file                : (bool) transcode the video & audio if necessary to ensure\n                                             common web browser support. FFmpeg is required\n        --allow_duplicate_name=<allow_duplicate_name> : (bool) create new claim even if one already exists with\n                                              given name. default: false.\n            --fee_currency=<fee_currency>  : (string) specify fee currency\n            --fee_amount=<fee_amount>      : (decimal) content download fee\n            --fee_address=<fee_address>    : (str) address where to send fee payments, will use\n                                                   value from --claim_address if not provided\n            --title=<title>                : (str) title of the publication\n            --description=<description>    : (str) description of the publication\n            --author=<author>              : (str) author of the publication. The usage for this field is not\n                                             the same as for channels. The author field is used to credit an author\n                                             who is not the publisher and is not represented by the channel. For\n                                             example, a pdf file of 'The Odyssey' has an author of 'Homer' but may\n                                             by published to a channel such as '@classics', or to no channel at all\n            --tags=<tags>                  : (list) add content tags\n            --languages=<languages>        : (list) languages used by the channel,\n                                                    using RFC 5646 format, eg:\n                                                    for English `--languages=en`\n                                                    for Spanish (Spain) `--languages=es-ES`\n                                                    for Spanish (Mexican) `--languages=es-MX`\n                                                    for Chinese (Simplified) `--languages=zh-Hans`\n                                                    for Chinese (Traditional) `--languages=zh-Hant`\n            --locations=<locations>        : (list) locations relevant to the stream, consisting of 2 letter\n                                                    `country` code and a `state`, `city` and a postal\n                                                    `code` along with a `latitude` and `longitude`.\n                                                    for JSON RPC: pass a dictionary with aforementioned\n                                                        attributes as keys, eg:\n                                                        ...\n                                                        \"locations\": [{'country': 'US', 'state': 'NH'}]\n                                                        ...\n                                                    for command line: pass a colon delimited list\n                                                        with values in the following order:\n\n                                                          \"COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE\"\n\n                                                        making sure to include colon for blank values, for\n                                                        example to provide only the city:\n\n                                                          ... --locations=\"::Manchester\"\n\n                                                        with all values set:\n\n                                                 ... --locations=\"US:NH:Manchester:03101:42.990605:-71.460989\"\n\n                                                        optionally, you can just pass the \"LATITUDE:LONGITUDE\":\n\n                                                          ... --locations=\"42.990605:-71.460989\"\n\n                                                        finally, you can also pass JSON string of dictionary\n                                                        on the command line as you would via JSON RPC\n\n                                                          ... --locations=\"{'country': 'US', 'state': 'NH'}\"\n\n            --license=<license>            : (str) publication license\n            --license_url=<license_url>    : (str) publication license url\n            --thumbnail_url=<thumbnail_url>: (str) thumbnail url\n            --release_time=<release_time>  : (int) original public release of content, seconds since UNIX epoch\n            --width=<width>                : (int) image/video width, automatically calculated from media file\n            --height=<height>              : (int) image/video height, automatically calculated from media file\n            --duration=<duration>          : (int) audio/video duration in seconds, automatically calculated\n            --sd_hash=<sd_hash>            : (str) sd_hash of stream\n            --channel_id=<channel_id>      : (str) claim id of the publisher channel\n            --channel_name=<channel_name>  : (str) name of the publisher channel\n            --channel_account_id=<channel_account_id>: (str) one or more account ids for accounts to look in\n                                                   for channel certificates, defaults to all accounts.\n            --account_id=<account_id>      : (str) account to use for holding the transaction\n            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet\n            --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction\n            --claim_address=<claim_address>: (str) address where the claim is sent to, if not specified\n                                                   it will be determined automatically from the account\n            --preview                      : (bool) do not broadcast the transaction\n            --blocking                     : (bool) wait until transaction is in mempool\n\n        Returns: {Transaction}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        assert not wallet.is_locked, \"Cannot spend funds with locked wallet, unlock first.\"\n        self.valid_stream_name_or_error(name)\n        account = wallet.get_account_or_default(account_id)\n        funding_accounts = wallet.get_accounts_or_all(funding_account_ids)\n        channel = await self.get_channel_or_none(wallet, channel_account_id, channel_id, channel_name, for_signing=True)\n        amount = self.get_dewies_or_error('bid', bid, positive_value=True)\n        claim_address = await self.get_receiving_address(claim_address, account)\n        kwargs['fee_address'] = self.get_fee_address(kwargs, claim_address)\n\n        claims = await account.get_claims(claim_name=name)\n        if len(claims) > 0:\n            if not allow_duplicate_name:\n                # TODO: use error from lbry.error\n                raise Exception(\n                    f\"You already have a stream claim published under the name '{name}'. \"\n                    f\"Use --allow-duplicate-name flag to override.\"\n                )\n\n        if file_path is not None:\n            file_path, spec = await self._video_file_analyzer.verify_or_repair(\n                validate_file, optimize_file, file_path, ignore_non_video=True\n            )\n            kwargs.update(spec)\n\n        claim = Claim()\n        if file_path is not None:\n            claim.stream.update(file_path=file_path, sd_hash='0' * 96, **kwargs)\n        else:\n            claim.stream.update(**kwargs)\n        tx = await Transaction.claim_create(\n            name, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel\n        )\n        new_txo = tx.outputs[0]\n\n        file_stream = None\n        if not preview and file_path is not None:\n            file_stream = await self.file_manager.create_stream(file_path)\n            claim.stream.source.sd_hash = file_stream.sd_hash\n            new_txo.script.generate()\n\n        if channel:\n            new_txo.sign(channel)\n        await tx.sign(funding_accounts)\n\n        if not preview:\n            await self.broadcast_or_release(tx, blocking)\n\n            async def save_claims():\n                await self.storage.save_claims([self._old_get_temp_claim_info(\n                    tx, new_txo, claim_address, claim, name\n                )])\n                if file_path is not None:\n                    await self.storage.save_content_claim(file_stream.stream_hash, new_txo.id)\n\n            self.component_manager.loop.create_task(save_claims())\n            self.component_manager.loop.create_task(self.analytics_manager.send_claim_action('publish'))\n        else:\n            await account.ledger.release_tx(tx)\n\n        return tx\n\n    @requires(WALLET_COMPONENT, FILE_MANAGER_COMPONENT, BLOB_COMPONENT, DATABASE_COMPONENT)\n    async def jsonrpc_stream_update(\n            self, claim_id, bid=None, file_path=None,\n            channel_id=None, channel_name=None, channel_account_id=None, clear_channel=False,\n            account_id=None, wallet_id=None, claim_address=None, funding_account_ids=None,\n            preview=False, blocking=False, replace=False, validate_file=False, optimize_file=False, **kwargs):\n        \"\"\"\n        Update an existing stream claim and if a new file is provided announce it to lbrynet.\n\n        Usage:\n            stream_update (<claim_id> | --claim_id=<claim_id>) [--bid=<bid>] [--file_path=<file_path>]\n                    [--validate_file] [--optimize_file]\n                    [--file_name=<file_name>] [--file_size=<file_size>] [--file_hash=<file_hash>]\n                    [--fee_currency=<fee_currency>] [--fee_amount=<fee_amount>]\n                    [--fee_address=<fee_address>] [--clear_fee]\n                    [--title=<title>] [--description=<description>] [--author=<author>]\n                    [--tags=<tags>...] [--clear_tags]\n                    [--languages=<languages>...] [--clear_languages]\n                    [--locations=<locations>...] [--clear_locations]\n                    [--license=<license>] [--license_url=<license_url>] [--thumbnail_url=<thumbnail_url>]\n                    [--release_time=<release_time>] [--width=<width>] [--height=<height>] [--duration=<duration>]\n                    [--sd_hash=<sd_hash>] [--channel_id=<channel_id> | --channel_name=<channel_name> | --clear_channel]\n                    [--channel_account_id=<channel_account_id>...]\n                    [--account_id=<account_id>] [--wallet_id=<wallet_id>]\n                    [--claim_address=<claim_address>] [--funding_account_ids=<funding_account_ids>...]\n                    [--preview] [--blocking] [--replace]\n\n        Options:\n            --claim_id=<claim_id>          : (str) id of the stream claim to update\n            --bid=<bid>                    : (decimal) amount to back the claim\n            --file_path=<file_path>        : (str) path to file to be associated with name.\n            --validate_file                : (bool) validate that the video container and encodings match\n                                             common web browser support or that optimization succeeds if specified.\n                                             FFmpeg is required and file_path must be specified.\n            --optimize_file                : (bool) transcode the video & audio if necessary to ensure common\n                                             web browser support. FFmpeg is required and file_path must be specified.\n            --file_name=<file_name>        : (str) override file name, defaults to name from file_path.\n            --file_size=<file_size>        : (str) override file size, otherwise automatically computed.\n            --file_hash=<file_hash>        : (str) override file hash, otherwise automatically computed.\n            --fee_currency=<fee_currency>  : (string) specify fee currency\n            --fee_amount=<fee_amount>      : (decimal) content download fee\n            --fee_address=<fee_address>    : (str) address where to send fee payments, will use\n                                                   value from --claim_address if not provided\n            --clear_fee                    : (bool) clear previously set fee\n            --title=<title>                : (str) title of the publication\n            --description=<description>    : (str) description of the publication\n            --author=<author>              : (str) author of the publication. The usage for this field is not\n                                             the same as for channels. The author field is used to credit an author\n                                             who is not the publisher and is not represented by the channel. For\n                                             example, a pdf file of 'The Odyssey' has an author of 'Homer' but may\n                                             by published to a channel such as '@classics', or to no channel at all\n            --tags=<tags>                  : (list) add content tags\n            --clear_tags                   : (bool) clear existing tags (prior to adding new ones)\n            --languages=<languages>        : (list) languages used by the channel,\n                                                    using RFC 5646 format, eg:\n                                                    for English `--languages=en`\n                                                    for Spanish (Spain) `--languages=es-ES`\n                                                    for Spanish (Mexican) `--languages=es-MX`\n                                                    for Chinese (Simplified) `--languages=zh-Hans`\n                                                    for Chinese (Traditional) `--languages=zh-Hant`\n            --clear_languages              : (bool) clear existing languages (prior to adding new ones)\n            --locations=<locations>        : (list) locations relevant to the stream, consisting of 2 letter\n                                                    `country` code and a `state`, `city` and a postal\n                                                    `code` along with a `latitude` and `longitude`.\n                                                    for JSON RPC: pass a dictionary with aforementioned\n                                                        attributes as keys, eg:\n                                                        ...\n                                                        \"locations\": [{'country': 'US', 'state': 'NH'}]\n                                                        ...\n                                                    for command line: pass a colon delimited list\n                                                        with values in the following order:\n\n                                                          \"COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE\"\n\n                                                        making sure to include colon for blank values, for\n                                                        example to provide only the city:\n\n                                                          ... --locations=\"::Manchester\"\n\n                                                        with all values set:\n\n                                                 ... --locations=\"US:NH:Manchester:03101:42.990605:-71.460989\"\n\n                                                        optionally, you can just pass the \"LATITUDE:LONGITUDE\":\n\n                                                          ... --locations=\"42.990605:-71.460989\"\n\n                                                        finally, you can also pass JSON string of dictionary\n                                                        on the command line as you would via JSON RPC\n\n                                                          ... --locations=\"{'country': 'US', 'state': 'NH'}\"\n\n            --clear_locations              : (bool) clear existing locations (prior to adding new ones)\n            --license=<license>            : (str) publication license\n            --license_url=<license_url>    : (str) publication license url\n            --thumbnail_url=<thumbnail_url>: (str) thumbnail url\n            --release_time=<release_time>  : (int) original public release of content, seconds since UNIX epoch\n            --width=<width>                : (int) image/video width, automatically calculated from media file\n            --height=<height>              : (int) image/video height, automatically calculated from media file\n            --duration=<duration>          : (int) audio/video duration in seconds, automatically calculated\n            --sd_hash=<sd_hash>            : (str) sd_hash of stream\n            --channel_id=<channel_id>      : (str) claim id of the publisher channel\n            --channel_name=<channel_name>  : (str) name of the publisher channel\n            --clear_channel                : (bool) remove channel signature\n          --channel_account_id=<channel_account_id>: (str) one or more account ids for accounts to look in\n                                                   for channel certificates, defaults to all accounts.\n            --account_id=<account_id>      : (str) account in which to look for stream (default: all)\n            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet\n          --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction\n            --claim_address=<claim_address>: (str) address where the claim is sent to, if not specified\n                                                   it will be determined automatically from the account\n            --preview                      : (bool) do not broadcast the transaction\n            --blocking                     : (bool) wait until transaction is in mempool\n            --replace                      : (bool) instead of modifying specific values on\n                                                    the stream, this will clear all existing values\n                                                    and only save passed in values, useful for form\n                                                    submissions where all values are always set\n\n        Returns: {Transaction}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        assert not wallet.is_locked, \"Cannot spend funds with locked wallet, unlock first.\"\n        funding_accounts = wallet.get_accounts_or_all(funding_account_ids)\n        if account_id:\n            account = wallet.get_account_or_error(account_id)\n            accounts = [account]\n        else:\n            account = wallet.default_account\n            accounts = wallet.accounts\n\n        existing_claims = await self.ledger.get_claims(\n            wallet=wallet, accounts=accounts, claim_id=claim_id\n        )\n        if len(existing_claims) != 1:\n            account_ids = ', '.join(f\"'{account.id}'\" for account in accounts)\n            raise InputValueError(\n                f\"Can't find the stream '{claim_id}' in account(s) {account_ids}.\"\n            )\n\n        old_txo = existing_claims[0]\n        if not old_txo.claim.is_stream and not old_txo.claim.is_repost:\n            # in principle it should work with any type of claim, but its safer to\n            # limit it to ones we know won't be broken. in the future we can expand\n            # this if we have a test case for e.g. channel or support claims\n            raise InputValueError(\n                f\"A claim with id '{claim_id}' was found but it is not a stream or repost claim.\"\n            )\n\n        if bid is not None:\n            amount = self.get_dewies_or_error('bid', bid, positive_value=True)\n        else:\n            amount = old_txo.amount\n\n        if claim_address is not None:\n            self.valid_address_or_error(claim_address)\n        else:\n            claim_address = old_txo.get_address(account.ledger)\n\n        channel = None\n        if not clear_channel and (channel_id or channel_name):\n            channel = await self.get_channel_or_error(\n                wallet, channel_account_id, channel_id, channel_name, for_signing=True)\n        elif old_txo.claim.is_signed and not clear_channel and not replace:\n            channel = old_txo.channel\n\n        fee_address = self.get_fee_address(kwargs, claim_address)\n        if fee_address:\n            kwargs['fee_address'] = fee_address\n\n        file_path, spec = await self._video_file_analyzer.verify_or_repair(\n            validate_file, optimize_file, file_path, ignore_non_video=True\n        )\n        kwargs.update(spec)\n\n        if replace:\n            claim = Claim()\n            if old_txo.claim.is_stream:\n                if old_txo.claim.stream.has_source:\n                    claim.stream.message.source.CopyFrom(\n                        old_txo.claim.stream.message.source\n                    )\n                stream_type = old_txo.claim.stream.stream_type\n                if stream_type:\n                    old_stream_type = getattr(old_txo.claim.stream.message, stream_type)\n                    new_stream_type = getattr(claim.stream.message, stream_type)\n                    new_stream_type.CopyFrom(old_stream_type)\n        else:\n            claim = Claim.from_bytes(old_txo.claim.to_bytes())\n\n        if old_txo.claim.is_stream:\n            claim.stream.update(file_path=file_path, **kwargs)\n        elif old_txo.claim.is_repost:\n            claim.repost.update(**kwargs)\n\n        if clear_channel:\n            claim.clear_signature()\n        tx = await Transaction.claim_update(\n            old_txo, claim, amount, claim_address, funding_accounts, funding_accounts[0],\n            channel if not clear_channel else None\n        )\n\n        new_txo = tx.outputs[0]\n        stream_hash = None\n        if not preview and old_txo.claim.is_stream:\n            old_stream = self.file_manager.get_filtered(sd_hash=old_txo.claim.stream.source.sd_hash)\n            old_stream = old_stream[0] if old_stream else None\n            if file_path is not None:\n                if old_stream:\n                    await self.file_manager.delete(old_stream, delete_file=False)\n                file_stream = await self.file_manager.create_stream(file_path)\n                new_txo.claim.stream.source.sd_hash = file_stream.sd_hash\n                new_txo.script.generate()\n                stream_hash = file_stream.stream_hash\n            elif old_stream:\n                stream_hash = old_stream.stream_hash\n\n        if channel:\n            new_txo.sign(channel)\n        await tx.sign(funding_accounts)\n\n        if not preview:\n            await self.broadcast_or_release(tx, blocking)\n\n            async def save_claims():\n                await self.storage.save_claims([self._old_get_temp_claim_info(\n                    tx, new_txo, claim_address, new_txo.claim, new_txo.claim_name\n                )])\n                if stream_hash:\n                    await self.storage.save_content_claim(stream_hash, new_txo.id)\n\n            self.component_manager.loop.create_task(save_claims())\n            self.component_manager.loop.create_task(self.analytics_manager.send_claim_action('publish'))\n        else:\n            await account.ledger.release_tx(tx)\n\n        return tx\n\n    @requires(WALLET_COMPONENT)\n    async def jsonrpc_stream_abandon(\n            self, claim_id=None, txid=None, nout=None, account_id=None, wallet_id=None,\n            preview=False, blocking=False):\n        \"\"\"\n        Abandon one of my stream claims.\n\n        Usage:\n            stream_abandon [<claim_id> | --claim_id=<claim_id>]\n                           [<txid> | --txid=<txid>] [<nout> | --nout=<nout>]\n                           [--account_id=<account_id>] [--wallet_id=<wallet_id>]\n                           [--preview] [--blocking]\n\n        Options:\n            --claim_id=<claim_id>     : (str) claim_id of the claim to abandon\n            --txid=<txid>             : (str) txid of the claim to abandon\n            --nout=<nout>             : (int) nout of the claim to abandon\n            --account_id=<account_id> : (str) id of the account to use\n            --wallet_id=<wallet_id>   : (str) restrict operation to specific wallet\n            --preview                 : (bool) do not broadcast the transaction\n            --blocking                : (bool) wait until abandon is in mempool\n\n        Returns: {Transaction}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        assert not wallet.is_locked, \"Cannot spend funds with locked wallet, unlock first.\"\n        if account_id:\n            account = wallet.get_account_or_error(account_id)\n            accounts = [account]\n        else:\n            account = wallet.default_account\n            accounts = wallet.accounts\n\n        if txid is not None and nout is not None:\n            claims = await self.ledger.get_claims(\n                wallet=wallet, accounts=accounts, **{'txo.txid': txid, 'txo.position': nout}\n            )\n        elif claim_id is not None:\n            claims = await self.ledger.get_claims(\n                wallet=wallet, accounts=accounts, claim_id=claim_id\n            )\n        else:\n            # TODO: use error from lbry.error\n            raise Exception('Must specify claim_id, or txid and nout')\n\n        if not claims:\n            # TODO: use error from lbry.error\n            raise Exception('No claim found for the specified claim_id or txid:nout')\n\n        tx = await Transaction.create(\n            [Input.spend(txo) for txo in claims], [], accounts, account\n        )\n\n        if not preview:\n            await self.broadcast_or_release(tx, blocking)\n            self.component_manager.loop.create_task(self.analytics_manager.send_claim_action('abandon'))\n        else:\n            await self.ledger.release_tx(tx)\n\n        return tx\n\n    @requires(WALLET_COMPONENT)\n    def jsonrpc_stream_list(self, *args, **kwargs):\n        \"\"\"\n        List my stream claims.\n\n        Usage:\n            stream_list [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>]\n                        [--name=<name>...] [--claim_id=<claim_id>...] [--is_spent]\n                        [--page=<page>] [--page_size=<page_size>] [--resolve] [--no_totals]\n\n        Options:\n            --name=<name>              : (str or list) stream name\n            --claim_id=<claim_id>      : (str or list) stream id\n            --is_spent                 : (bool) shows previous stream updates and abandons\n            --account_id=<account_id>  : (str) id of the account to query\n            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet\n            --page=<page>              : (int) page to return during paginating\n            --page_size=<page_size>    : (int) number of items on page during pagination\n            --resolve                  : (bool) resolves each stream to provide additional metadata\n            --no_totals                : (bool) do not calculate the total number of pages and items in result set\n                                                (significant performance boost)\n\n        Returns: {Paginated[Output]}\n        \"\"\"\n        kwargs['type'] = 'stream'\n        if 'is_spent' not in kwargs:\n            kwargs['is_not_spent'] = True\n        return self.jsonrpc_txo_list(*args, **kwargs)\n\n    @requires(WALLET_COMPONENT, EXCHANGE_RATE_MANAGER_COMPONENT, BLOB_COMPONENT,\n              DHT_COMPONENT, DATABASE_COMPONENT)\n    def jsonrpc_stream_cost_estimate(self, uri):\n        \"\"\"\n        Get estimated cost for a lbry stream\n\n        Usage:\n            stream_cost_estimate (<uri> | --uri=<uri>)\n\n        Options:\n            --uri=<uri>      : (str) uri to use\n\n        Returns:\n            (float) Estimated cost in lbry credits, returns None if uri is not\n                resolvable\n        \"\"\"\n        return self.get_est_cost_from_uri(uri)\n\n    COLLECTION_DOC = \"\"\"\n    Create, update, list, resolve, and abandon collections.\n    \"\"\"\n\n    @requires(WALLET_COMPONENT)\n    async def jsonrpc_collection_create(\n            self, name, bid, claims, allow_duplicate_name=False,\n            channel_id=None, channel_name=None, channel_account_id=None,\n            account_id=None, wallet_id=None, claim_address=None, funding_account_ids=None,\n            preview=False, blocking=False, **kwargs):\n        \"\"\"\n        Create a new collection.\n\n        Usage:\n            collection_create (<name> | --name=<name>) (<bid> | --bid=<bid>)\n                    (--claims=<claims>...)\n                    [--allow_duplicate_name]\n                    [--title=<title>] [--description=<description>]\n                    [--tags=<tags>...] [--languages=<languages>...] [--locations=<locations>...]\n                    [--thumbnail_url=<thumbnail_url>]\n                    [--channel_id=<channel_id> | --channel_name=<channel_name>]\n                    [--channel_account_id=<channel_account_id>...]\n                    [--account_id=<account_id>] [--wallet_id=<wallet_id>]\n                    [--claim_address=<claim_address>] [--funding_account_ids=<funding_account_ids>...]\n                    [--preview] [--blocking]\n\n        Options:\n            --name=<name>                  : (str) name of the collection\n            --bid=<bid>                    : (decimal) amount to back the claim\n            --claims=<claims>              : (list) claim ids to be included in the collection\n            --allow_duplicate_name         : (bool) create new collection even if one already exists with\n                                                    given name. default: false.\n            --title=<title>                : (str) title of the collection\n            --description=<description>    : (str) description of the collection\n            --tags=<tags>                  : (list) content tags\n            --clear_languages              : (bool) clear existing languages (prior to adding new ones)\n            --languages=<languages>        : (list) languages used by the collection,\n                                                    using RFC 5646 format, eg:\n                                                    for English `--languages=en`\n                                                    for Spanish (Spain) `--languages=es-ES`\n                                                    for Spanish (Mexican) `--languages=es-MX`\n                                                    for Chinese (Simplified) `--languages=zh-Hans`\n                                                    for Chinese (Traditional) `--languages=zh-Hant`\n            --locations=<locations>        : (list) locations of the collection, consisting of 2 letter\n                                                    `country` code and a `state`, `city` and a postal\n                                                    `code` along with a `latitude` and `longitude`.\n                                                    for JSON RPC: pass a dictionary with aforementioned\n                                                        attributes as keys, eg:\n                                                        ...\n                                                        \"locations\": [{'country': 'US', 'state': 'NH'}]\n                                                        ...\n                                                    for command line: pass a colon delimited list\n                                                        with values in the following order:\n\n                                                          \"COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE\"\n\n                                                        making sure to include colon for blank values, for\n                                                        example to provide only the city:\n\n                                                          ... --locations=\"::Manchester\"\n\n                                                        with all values set:\n\n                                                 ... --locations=\"US:NH:Manchester:03101:42.990605:-71.460989\"\n\n                                                        optionally, you can just pass the \"LATITUDE:LONGITUDE\":\n\n                                                          ... --locations=\"42.990605:-71.460989\"\n\n                                                        finally, you can also pass JSON string of dictionary\n                                                        on the command line as you would via JSON RPC\n\n                                                          ... --locations=\"{'country': 'US', 'state': 'NH'}\"\n\n            --thumbnail_url=<thumbnail_url>: (str) thumbnail url\n            --channel_id=<channel_id>      : (str) claim id of the publisher channel\n            --channel_name=<channel_name>  : (str) name of the publisher channel\n            --channel_account_id=<channel_account_id>: (str) one or more account ids for accounts to look in\n                                                   for channel certificates, defaults to all accounts.\n            --account_id=<account_id>      : (str) account to use for holding the transaction\n            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet\n            --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction\n            --claim_address=<claim_address>: (str) address where the collection is sent to, if not specified\n                                                   it will be determined automatically from the account\n            --preview                      : (bool) do not broadcast the transaction\n            --blocking                     : (bool) wait until transaction is in mempool\n\n        Returns: {Transaction}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        account = wallet.get_account_or_default(account_id)\n        funding_accounts = wallet.get_accounts_or_all(funding_account_ids)\n        self.valid_collection_name_or_error(name)\n        channel = await self.get_channel_or_none(wallet, channel_account_id, channel_id, channel_name, for_signing=True)\n        amount = self.get_dewies_or_error('bid', bid, positive_value=True)\n        claim_address = await self.get_receiving_address(claim_address, account)\n\n        existing_collections = await self.ledger.get_collections(accounts=wallet.accounts, claim_name=name)\n        if len(existing_collections) > 0:\n            if not allow_duplicate_name:\n                # TODO: use error from lbry.error\n                raise Exception(\n                    f\"You already have a collection under the name '{name}'. \"\n                    f\"Use --allow-duplicate-name flag to override.\"\n                )\n\n        claim = Claim()\n        claim.collection.update(claims=claims, **kwargs)\n        tx = await Transaction.claim_create(\n            name, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel\n        )\n        new_txo = tx.outputs[0]\n\n        if channel:\n            new_txo.sign(channel)\n        await tx.sign(funding_accounts)\n\n        if not preview:\n            await self.broadcast_or_release(tx, blocking)\n            self.component_manager.loop.create_task(self.analytics_manager.send_claim_action('publish'))\n        else:\n            await account.ledger.release_tx(tx)\n\n        return tx\n\n    @requires(WALLET_COMPONENT)\n    async def jsonrpc_collection_update(\n            self, claim_id, bid=None,\n            channel_id=None, channel_name=None, channel_account_id=None, clear_channel=False,\n            account_id=None, wallet_id=None, claim_address=None, funding_account_ids=None,\n            preview=False, blocking=False, replace=False, **kwargs):\n        \"\"\"\n        Update an existing collection claim.\n\n        Usage:\n            collection_update (<claim_id> | --claim_id=<claim_id>) [--bid=<bid>]\n                            [--claims=<claims>...] [--clear_claims]\n                           [--title=<title>] [--description=<description>]\n                           [--tags=<tags>...] [--clear_tags]\n                           [--languages=<languages>...] [--clear_languages]\n                           [--locations=<locations>...] [--clear_locations]\n                           [--thumbnail_url=<thumbnail_url>] [--cover_url=<cover_url>]\n                           [--channel_id=<channel_id> | --channel_name=<channel_name>]\n                           [--channel_account_id=<channel_account_id>...]\n                           [--account_id=<account_id>] [--wallet_id=<wallet_id>]\n                           [--claim_address=<claim_address>]\n                           [--funding_account_ids=<funding_account_ids>...]\n                           [--preview] [--blocking] [--replace]\n\n        Options:\n            --claim_id=<claim_id>          : (str) claim_id of the collection to update\n            --bid=<bid>                    : (decimal) amount to back the claim\n            --claims=<claims>              : (list) claim ids\n            --clear_claims                 : (bool) clear existing claim references (prior to adding new ones)\n            --title=<title>                : (str) title of the collection\n            --description=<description>    : (str) description of the collection\n            --tags=<tags>                  : (list) add content tags\n            --clear_tags                   : (bool) clear existing tags (prior to adding new ones)\n            --languages=<languages>        : (list) languages used by the collection,\n                                                    using RFC 5646 format, eg:\n                                                    for English `--languages=en`\n                                                    for Spanish (Spain) `--languages=es-ES`\n                                                    for Spanish (Mexican) `--languages=es-MX`\n                                                    for Chinese (Simplified) `--languages=zh-Hans`\n                                                    for Chinese (Traditional) `--languages=zh-Hant`\n            --clear_languages              : (bool) clear existing languages (prior to adding new ones)\n            --locations=<locations>        : (list) locations of the collection, consisting of 2 letter\n                                                    `country` code and a `state`, `city` and a postal\n                                                    `code` along with a `latitude` and `longitude`.\n                                                    for JSON RPC: pass a dictionary with aforementioned\n                                                        attributes as keys, eg:\n                                                        ...\n                                                        \"locations\": [{'country': 'US', 'state': 'NH'}]\n                                                        ...\n                                                    for command line: pass a colon delimited list\n                                                        with values in the following order:\n\n                                                          \"COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE\"\n\n                                                        making sure to include colon for blank values, for\n                                                        example to provide only the city:\n\n                                                          ... --locations=\"::Manchester\"\n\n                                                        with all values set:\n\n                                                 ... --locations=\"US:NH:Manchester:03101:42.990605:-71.460989\"\n\n                                                        optionally, you can just pass the \"LATITUDE:LONGITUDE\":\n\n                                                          ... --locations=\"42.990605:-71.460989\"\n\n                                                        finally, you can also pass JSON string of dictionary\n                                                        on the command line as you would via JSON RPC\n\n                                                          ... --locations=\"{'country': 'US', 'state': 'NH'}\"\n\n            --clear_locations              : (bool) clear existing locations (prior to adding new ones)\n            --thumbnail_url=<thumbnail_url>: (str) thumbnail url\n            --channel_id=<channel_id>      : (str) claim id of the publisher channel\n            --channel_name=<channel_name>  : (str) name of the publisher channel\n            --channel_account_id=<channel_account_id>: (str) one or more account ids for accounts to look in\n                                                   for channel certificates, defaults to all accounts.\n            --account_id=<account_id>      : (str) account in which to look for collection (default: all)\n            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet\n          --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction\n            --claim_address=<claim_address>: (str) address where the collection is sent\n            --preview                      : (bool) do not broadcast the transaction\n            --blocking                     : (bool) wait until transaction is in mempool\n            --replace                      : (bool) instead of modifying specific values on\n                                                    the collection, this will clear all existing values\n                                                    and only save passed in values, useful for form\n                                                    submissions where all values are always set\n\n        Returns: {Transaction}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        funding_accounts = wallet.get_accounts_or_all(funding_account_ids)\n        if account_id:\n            account = wallet.get_account_or_error(account_id)\n            accounts = [account]\n        else:\n            account = wallet.default_account\n            accounts = wallet.accounts\n\n        existing_collections = await self.ledger.get_collections(\n            wallet=wallet, accounts=accounts, claim_id=claim_id\n        )\n        if len(existing_collections) != 1:\n            account_ids = ', '.join(f\"'{account.id}'\" for account in accounts)\n            # TODO: use error from lbry.error\n            raise Exception(\n                f\"Can't find the collection '{claim_id}' in account(s) {account_ids}.\"\n            )\n        old_txo = existing_collections[0]\n        if not old_txo.claim.is_collection:\n            # TODO: use error from lbry.error\n            raise Exception(\n                f\"A claim with id '{claim_id}' was found but it is not a collection.\"\n            )\n\n        if bid is not None:\n            amount = self.get_dewies_or_error('bid', bid, positive_value=True)\n        else:\n            amount = old_txo.amount\n\n        if claim_address is not None:\n            self.valid_address_or_error(claim_address)\n        else:\n            claim_address = old_txo.get_address(account.ledger)\n\n        channel = None\n        if channel_id or channel_name:\n            channel = await self.get_channel_or_error(\n                wallet, channel_account_id, channel_id, channel_name, for_signing=True)\n        elif old_txo.claim.is_signed and not clear_channel and not replace:\n            channel = old_txo.channel\n\n        if replace:\n            claim = Claim()\n            claim.collection.update(**kwargs)\n        else:\n            claim = Claim.from_bytes(old_txo.claim.to_bytes())\n            claim.collection.update(**kwargs)\n        tx = await Transaction.claim_update(\n            old_txo, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel\n        )\n        new_txo = tx.outputs[0]\n\n        if channel:\n            new_txo.sign(channel)\n        await tx.sign(funding_accounts)\n\n        if not preview:\n            await self.broadcast_or_release(tx, blocking)\n            self.component_manager.loop.create_task(self.analytics_manager.send_claim_action('publish'))\n        else:\n            await account.ledger.release_tx(tx)\n\n        return tx\n\n    @requires(WALLET_COMPONENT)\n    async def jsonrpc_collection_abandon(self, *args, **kwargs):\n        \"\"\"\n        Abandon one of my collection claims.\n\n        Usage:\n            collection_abandon [<claim_id> | --claim_id=<claim_id>]\n                            [<txid> | --txid=<txid>] [<nout> | --nout=<nout>]\n                            [--account_id=<account_id>] [--wallet_id=<wallet_id>]\n                            [--preview] [--blocking]\n\n        Options:\n            --claim_id=<claim_id>     : (str) claim_id of the claim to abandon\n            --txid=<txid>             : (str) txid of the claim to abandon\n            --nout=<nout>             : (int) nout of the claim to abandon\n            --account_id=<account_id> : (str) id of the account to use\n            --wallet_id=<wallet_id>   : (str) restrict operation to specific wallet\n            --preview                 : (bool) do not broadcast the transaction\n            --blocking                : (bool) wait until abandon is in mempool\n\n        Returns: {Transaction}\n        \"\"\"\n        return await self.jsonrpc_stream_abandon(*args, **kwargs)\n\n    @requires(WALLET_COMPONENT)\n    def jsonrpc_collection_list(\n            self, resolve_claims=0, resolve=False, account_id=None,\n            wallet_id=None, page=None, page_size=None):\n        \"\"\"\n        List my collection claims.\n\n        Usage:\n            collection_list [--resolve_claims=<resolve_claims>] [--resolve] [<account_id> | --account_id=<account_id>]\n                [--wallet_id=<wallet_id>] [--page=<page>] [--page_size=<page_size>]\n\n        Options:\n            --resolve                         : (bool) resolve collection claim\n            --resolve_claims=<resolve_claims> : (int) resolve every claim\n            --account_id=<account_id>         : (str) id of the account to use\n            --wallet_id=<wallet_id>           : (str) restrict results to specific wallet\n            --page=<page>                     : (int) page to return during paginating\n            --page_size=<page_size>           : (int) number of items on page during pagination\n\n        Returns: {Paginated[Output]}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        if account_id:\n            account = wallet.get_account_or_error(account_id)\n            collections = account.get_collections\n            collection_count = account.get_collection_count\n        else:\n            collections = partial(self.ledger.get_collections, wallet=wallet, accounts=wallet.accounts)\n            collection_count = partial(self.ledger.get_collection_count, wallet=wallet, accounts=wallet.accounts)\n        return paginate_rows(\n            collections, collection_count, page, page_size,\n            resolve=resolve, resolve_claims=resolve_claims\n        )\n\n    async def jsonrpc_collection_resolve(\n            self, claim_id=None, url=None, wallet_id=None, page=1, page_size=DEFAULT_PAGE_SIZE):\n        \"\"\"\n        Resolve claims in the collection.\n\n        Usage:\n            collection_resolve (--claim_id=<claim_id> | --url=<url>)\n                [--wallet_id=<wallet_id>] [--page=<page>] [--page_size=<page_size>]\n\n        Options:\n            --claim_id=<claim_id>      : (str) claim id of the collection\n            --url=<url>                : (str) url of the collection\n            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet\n            --page=<page>              : (int) page to return during paginating\n            --page_size=<page_size>    : (int) number of items on page during pagination\n\n        Returns: {Paginated[Output]}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n\n        if claim_id:\n            txo = await self.ledger.get_claim_by_claim_id(claim_id, wallet.accounts)\n            if not isinstance(txo, Output) or not txo.is_claim:\n                # TODO: use error from lbry.error\n                raise Exception(f\"Could not find collection with claim_id '{claim_id}'.\")\n        elif url:\n            txo = (await self.ledger.resolve(wallet.accounts, [url]))[url]\n            if not isinstance(txo, Output) or not txo.is_claim:\n                # TODO: use error from lbry.error\n                raise Exception(f\"Could not find collection with url '{url}'.\")\n        else:\n            # TODO: use error from lbry.error\n            raise Exception(\"Missing argument claim_id or url.\")\n\n        page_num, page_size = abs(page), min(abs(page_size), 50)\n        items = await self.ledger.resolve_collection(txo, page_size * (page_num - 1), page_size)\n        total_items = len(txo.claim.collection.claims.ids)\n\n        return {\n            \"items\": items,\n            \"total_pages\": int((total_items + (page_size - 1)) / page_size),\n            \"total_items\": total_items,\n            \"page_size\": page_size,\n            \"page\": page\n        }\n\n    SUPPORT_DOC = \"\"\"\n    Create, list and abandon all types of supports.\n    \"\"\"\n\n    @requires(WALLET_COMPONENT)\n    async def jsonrpc_support_create(\n            self, claim_id, amount, tip=False,\n            channel_id=None, channel_name=None, channel_account_id=None,\n            account_id=None, wallet_id=None, funding_account_ids=None,\n            comment=None, preview=False, blocking=False):\n        \"\"\"\n        Create a support or a tip for name claim.\n\n        Usage:\n            support_create (<claim_id> | --claim_id=<claim_id>) (<amount> | --amount=<amount>)\n                           [--tip] [--account_id=<account_id>] [--wallet_id=<wallet_id>]\n                           [--channel_id=<channel_id> | --channel_name=<channel_name>]\n                           [--channel_account_id=<channel_account_id>...] [--comment=<comment>]\n                           [--preview] [--blocking] [--funding_account_ids=<funding_account_ids>...]\n\n        Options:\n            --claim_id=<claim_id>         : (str) claim_id of the claim to support\n            --amount=<amount>             : (decimal) amount of support\n            --tip                         : (bool) send support to claim owner, default: false.\n            --channel_id=<channel_id>     : (str) claim id of the supporters identity channel\n            --channel_name=<channel_name> : (str) name of the supporters identity channel\n          --channel_account_id=<channel_account_id>: (str) one or more account ids for accounts to look in\n                                                   for channel certificates, defaults to all accounts.\n            --account_id=<account_id>     : (str) account to use for holding the transaction\n            --wallet_id=<wallet_id>       : (str) restrict operation to specific wallet\n          --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction\n            --comment=<comment>           : (str) add a comment to the support\n            --preview                     : (bool) do not broadcast the transaction\n            --blocking                    : (bool) wait until transaction is in mempool\n\n        Returns: {Transaction}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        assert not wallet.is_locked, \"Cannot spend funds with locked wallet, unlock first.\"\n        funding_accounts = wallet.get_accounts_or_all(funding_account_ids)\n        channel = await self.get_channel_or_none(wallet, channel_account_id, channel_id, channel_name, for_signing=True)\n        amount = self.get_dewies_or_error(\"amount\", amount)\n        claim = await self.ledger.get_claim_by_claim_id(claim_id)\n        claim_address = claim.get_address(self.ledger)\n        if not tip:\n            account = wallet.get_account_or_default(account_id)\n            claim_address = await account.receiving.get_or_create_usable_address()\n\n        tx = await Transaction.support(\n            claim.claim_name, claim_id, amount, claim_address, funding_accounts, funding_accounts[0], channel,\n            comment=comment\n        )\n        new_txo = tx.outputs[0]\n\n        if channel:\n            new_txo.sign(channel)\n        await tx.sign(funding_accounts)\n\n        if not preview:\n            await self.broadcast_or_release(tx, blocking)\n            await self.storage.save_supports({claim_id: [{\n                'txid': tx.id,\n                'nout': tx.position,\n                'address': claim_address,\n                'claim_id': claim_id,\n                'amount': dewies_to_lbc(new_txo.amount)\n            }]})\n            self.component_manager.loop.create_task(self.analytics_manager.send_claim_action('new_support'))\n        else:\n            await self.ledger.release_tx(tx)\n\n        return tx\n\n    @requires(WALLET_COMPONENT)\n    def jsonrpc_support_list(self, *args, received=False, sent=False, staked=False, **kwargs):\n        \"\"\"\n        List staked supports and sent/received tips.\n\n        Usage:\n            support_list [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>]\n                         [--name=<name>...] [--claim_id=<claim_id>...]\n                         [--received | --sent | --staked] [--is_spent]\n                         [--page=<page>] [--page_size=<page_size>] [--no_totals]\n\n        Options:\n            --name=<name>              : (str or list) claim name\n            --claim_id=<claim_id>      : (str or list) claim id\n            --received                 : (bool) only show received (tips)\n            --sent                     : (bool) only show sent (tips)\n            --staked                   : (bool) only show my staked supports\n            --is_spent                 : (bool) show abandoned supports\n            --account_id=<account_id>  : (str) id of the account to query\n            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet\n            --page=<page>              : (int) page to return during paginating\n            --page_size=<page_size>    : (int) number of items on page during pagination\n            --no_totals                : (bool) do not calculate the total number of pages and items in result set\n                                                (significant performance boost)\n\n        Returns: {Paginated[Output]}\n        \"\"\"\n        kwargs['type'] = 'support'\n        if 'is_spent' not in kwargs:\n            kwargs['is_not_spent'] = True\n        if received:\n            kwargs['is_not_my_input'] = True\n            kwargs['is_my_output'] = True\n        elif sent:\n            kwargs['is_my_input'] = True\n            kwargs['is_not_my_output'] = True\n            # spent for not my outputs is undetermined\n            kwargs.pop('is_spent', None)\n            kwargs.pop('is_not_spent', None)\n        elif staked:\n            kwargs['is_my_input'] = True\n            kwargs['is_my_output'] = True\n        return self.jsonrpc_txo_list(*args, **kwargs)\n\n    @requires(WALLET_COMPONENT)\n    async def jsonrpc_support_abandon(\n            self, claim_id=None, txid=None, nout=None, keep=None,\n            account_id=None, wallet_id=None, preview=False, blocking=False):\n        \"\"\"\n        Abandon supports, including tips, of a specific claim, optionally\n        keeping some amount as supports.\n\n        Usage:\n            support_abandon [--claim_id=<claim_id>] [(--txid=<txid> --nout=<nout>)] [--keep=<keep>]\n                            [--account_id=<account_id>] [--wallet_id=<wallet_id>]\n                            [--preview] [--blocking]\n\n        Options:\n            --claim_id=<claim_id>     : (str) claim_id of the support to abandon\n            --txid=<txid>             : (str) txid of the claim to abandon\n            --nout=<nout>             : (int) nout of the claim to abandon\n            --keep=<keep>             : (decimal) amount of lbc to keep as support\n            --account_id=<account_id> : (str) id of the account to use\n            --wallet_id=<wallet_id>   : (str) restrict operation to specific wallet\n            --preview                 : (bool) do not broadcast the transaction\n            --blocking                : (bool) wait until abandon is in mempool\n\n        Returns: {Transaction}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        assert not wallet.is_locked, \"Cannot spend funds with locked wallet, unlock first.\"\n        if account_id:\n            account = wallet.get_account_or_error(account_id)\n            accounts = [account]\n        else:\n            account = wallet.default_account\n            accounts = wallet.accounts\n\n        if txid is not None and nout is not None:\n            supports = await self.ledger.get_supports(\n                wallet=wallet, accounts=accounts, **{'txo.txid': txid, 'txo.position': nout}\n            )\n        elif claim_id is not None:\n            supports = await self.ledger.get_supports(\n                wallet=wallet, accounts=accounts, claim_id=claim_id\n            )\n        else:\n            # TODO: use error from lbry.error\n            raise Exception('Must specify claim_id, or txid and nout')\n\n        if not supports:\n            # TODO: use error from lbry.error\n            raise Exception('No supports found for the specified claim_id or txid:nout')\n\n        if keep is not None:\n            keep = self.get_dewies_or_error('keep', keep)\n        else:\n            keep = 0\n\n        outputs = []\n        if keep > 0:\n            outputs = [\n                Output.pay_support_pubkey_hash(\n                    keep, supports[0].claim_name, supports[0].claim_id, supports[0].pubkey_hash\n                )\n            ]\n\n        tx = await Transaction.create(\n            [Input.spend(txo) for txo in supports], outputs, accounts, account\n        )\n\n        if not preview:\n            await self.broadcast_or_release(tx, blocking)\n            self.component_manager.loop.create_task(self.analytics_manager.send_claim_action('abandon'))\n        else:\n            await self.ledger.release_tx(tx)\n\n        return tx\n\n    TRANSACTION_DOC = \"\"\"\n    Transaction management.\n    \"\"\"\n\n    @requires(WALLET_COMPONENT)\n    def jsonrpc_transaction_list(self, account_id=None, wallet_id=None, page=None, page_size=None):\n        \"\"\"\n        List transactions belonging to wallet\n\n        Usage:\n            transaction_list [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>]\n                             [--page=<page>] [--page_size=<page_size>]\n\n        Options:\n            --account_id=<account_id>  : (str) id of the account to query\n            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet\n            --page=<page>              : (int) page to return during paginating\n            --page_size=<page_size>    : (int) number of items on page during pagination\n\n        Returns:\n            (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            }\n\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        if account_id:\n            account = wallet.get_account_or_error(account_id)\n            transactions = account.get_transaction_history\n            transaction_count = account.get_transaction_history_count\n        else:\n            transactions = partial(\n                self.ledger.get_transaction_history, wallet=wallet, accounts=wallet.accounts)\n            transaction_count = partial(\n                self.ledger.get_transaction_history_count, wallet=wallet, accounts=wallet.accounts)\n        return paginate_rows(transactions, transaction_count, page, page_size, read_only=True)\n\n    @requires(WALLET_COMPONENT)\n    def jsonrpc_transaction_show(self, txid):\n        \"\"\"\n        Get a decoded transaction from a txid\n\n        Usage:\n            transaction_show (<txid> | --txid=<txid>)\n\n        Options:\n            --txid=<txid>  : (str) txid of the transaction\n\n        Returns: {Transaction}\n        \"\"\"\n        return self.wallet_manager.get_transaction(txid)\n\n    TXO_DOC = \"\"\"\n    List and sum transaction outputs.\n    \"\"\"\n\n    @staticmethod\n    def _constrain_txo_from_kwargs(\n            constraints, type=None, txid=None,  # pylint: disable=redefined-builtin\n            claim_id=None, channel_id=None, not_channel_id=None,\n            name=None, reposted_claim_id=None,\n            is_spent=False, is_not_spent=False,\n            has_source=None, has_no_source=None,\n            is_my_input_or_output=None, exclude_internal_transfers=False,\n            is_my_output=None, is_not_my_output=None,\n            is_my_input=None, is_not_my_input=None):\n        if is_spent:\n            constraints['is_spent'] = True\n        elif is_not_spent:\n            constraints['is_spent'] = False\n        if has_source:\n            constraints['has_source'] = True\n        elif has_no_source:\n            constraints['has_source'] = False\n        constraints['exclude_internal_transfers'] = exclude_internal_transfers\n        if is_my_input_or_output is True:\n            constraints['is_my_input_or_output'] = True\n        else:\n            if is_my_input is True:\n                constraints['is_my_input'] = True\n            elif is_not_my_input is True:\n                constraints['is_my_input'] = False\n            if is_my_output is True:\n                constraints['is_my_output'] = True\n            elif is_not_my_output is True:\n                constraints['is_my_output'] = False\n        database.constrain_single_or_list(constraints, 'txo_type', type, lambda x: TXO_TYPES[x])\n        database.constrain_single_or_list(constraints, 'channel_id', channel_id)\n        database.constrain_single_or_list(constraints, 'channel_id', not_channel_id, negate=True)\n        database.constrain_single_or_list(constraints, 'claim_id', claim_id)\n        database.constrain_single_or_list(constraints, 'claim_name', name)\n        database.constrain_single_or_list(constraints, 'txid', txid)\n        database.constrain_single_or_list(constraints, 'reposted_claim_id', reposted_claim_id)\n        return constraints\n\n    @requires(WALLET_COMPONENT)\n    def jsonrpc_txo_list(\n            self, account_id=None, wallet_id=None, page=None, page_size=None,\n            resolve=False, order_by=None, no_totals=False, include_received_tips=False, **kwargs):\n        \"\"\"\n        List my transaction outputs.\n\n        Usage:\n            txo_list [--account_id=<account_id>] [--type=<type>...] [--txid=<txid>...] [--claim_id=<claim_id>...]\n                     [--channel_id=<channel_id>...] [--not_channel_id=<not_channel_id>...]\n                     [--name=<name>...] [--is_spent | --is_not_spent]\n                     [--is_my_input_or_output |\n                         [[--is_my_output | --is_not_my_output] [--is_my_input | --is_not_my_input]]\n                     ]\n                     [--exclude_internal_transfers] [--include_received_tips]\n                     [--wallet_id=<wallet_id>] [--page=<page>] [--page_size=<page_size>]\n                     [--resolve] [--order_by=<order_by>][--no_totals]\n\n        Options:\n            --type=<type>              : (str or list) claim type: stream, channel, support,\n                                         purchase, collection, repost, other\n            --txid=<txid>              : (str or list) transaction id of outputs\n            --claim_id=<claim_id>      : (str or list) claim id\n            --channel_id=<channel_id>  : (str or list) claims in this channel\n      --not_channel_id=<not_channel_id>: (str or list) claims not in this channel\n            --name=<name>              : (str or list) claim name\n            --is_spent                 : (bool) only show spent txos\n            --is_not_spent             : (bool) only show not spent txos\n            --is_my_input_or_output    : (bool) txos which have your inputs or your outputs,\n                                                if using this flag the other related flags\n                                                are ignored (--is_my_output, --is_my_input, etc)\n            --is_my_output             : (bool) show outputs controlled by you\n            --is_not_my_output         : (bool) show outputs not controlled by you\n            --is_my_input              : (bool) show outputs created by you\n            --is_not_my_input          : (bool) show outputs not created by you\n           --exclude_internal_transfers: (bool) excludes any outputs that are exactly this combination:\n                                                \"--is_my_input --is_my_output --type=other\"\n                                                this allows to exclude \"change\" payments, this\n                                                flag can be used in combination with any of the other flags\n            --include_received_tips    : (bool) calculate the amount of tips received for claim outputs\n            --account_id=<account_id>  : (str) id of the account to query\n            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet\n            --page=<page>              : (int) page to return during paginating\n            --page_size=<page_size>    : (int) number of items on page during pagination\n            --resolve                  : (bool) resolves each claim to provide additional metadata\n            --order_by=<order_by>      : (str) field to order by: 'name', 'height', 'amount' and 'none'\n            --no_totals                : (bool) do not calculate the total number of pages and items in result set\n                                                (significant performance boost)\n\n        Returns: {Paginated[Output]}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        if account_id:\n            account = wallet.get_account_or_error(account_id)\n            claims = account.get_txos\n            claim_count = account.get_txo_count\n        else:\n            claims = partial(self.ledger.get_txos, wallet=wallet, accounts=wallet.accounts, read_only=True)\n            claim_count = partial(self.ledger.get_txo_count, wallet=wallet, accounts=wallet.accounts, read_only=True)\n        constraints = {\n            'resolve': resolve,\n            'include_is_spent': True,\n            'include_is_my_input': True,\n            'include_is_my_output': True,\n            'include_received_tips': include_received_tips\n        }\n        if order_by is not None:\n            if order_by == 'name':\n                constraints['order_by'] = 'txo.claim_name'\n            elif order_by in ('height', 'amount', 'none'):\n                constraints['order_by'] = order_by\n            else:\n                # TODO: use error from lbry.error\n                raise ValueError(f\"'{order_by}' is not a valid --order_by value.\")\n        self._constrain_txo_from_kwargs(constraints, **kwargs)\n        return paginate_rows(claims, None if no_totals else claim_count, page, page_size, **constraints)\n\n    @requires(WALLET_COMPONENT)\n    async def jsonrpc_txo_spend(\n            self, account_id=None, wallet_id=None, batch_size=100,\n            include_full_tx=False, preview=False, blocking=False, **kwargs):\n        \"\"\"\n        Spend transaction outputs, batching into multiple transactions as necessary.\n\n        Usage:\n            txo_spend [--account_id=<account_id>] [--type=<type>...] [--txid=<txid>...] [--claim_id=<claim_id>...]\n                      [--channel_id=<channel_id>...] [--not_channel_id=<not_channel_id>...]\n                      [--name=<name>...] [--is_my_input | --is_not_my_input]\n                      [--exclude_internal_transfers] [--wallet_id=<wallet_id>]\n                      [--preview] [--blocking] [--batch_size=<batch_size>] [--include_full_tx]\n\n        Options:\n            --type=<type>              : (str or list) claim type: stream, channel, support,\n                                         purchase, collection, repost, other\n            --txid=<txid>              : (str or list) transaction id of outputs\n            --claim_id=<claim_id>      : (str or list) claim id\n            --channel_id=<channel_id>  : (str or list) claims in this channel\n      --not_channel_id=<not_channel_id>: (str or list) claims not in this channel\n            --name=<name>              : (str or list) claim name\n            --is_my_input              : (bool) show outputs created by you\n            --is_not_my_input          : (bool) show outputs not created by you\n           --exclude_internal_transfers: (bool) excludes any outputs that are exactly this combination:\n                                                \"--is_my_input --is_my_output --type=other\"\n                                                this allows to exclude \"change\" payments, this\n                                                flag can be used in combination with any of the other flags\n            --account_id=<account_id>  : (str) id of the account to query\n            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet\n            --preview                  : (bool) do not broadcast the transaction\n            --blocking                 : (bool) wait until abandon is in mempool\n            --batch_size=<batch_size>  : (int) number of txos to spend per transactions\n            --include_full_tx          : (bool) include entire tx in output and not just the txid\n\n        Returns: {List[Transaction]}\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        accounts = [wallet.get_account_or_error(account_id)] if account_id else wallet.accounts\n        txos = await self.ledger.get_txos(\n            wallet=wallet, accounts=accounts, read_only=True,\n            no_tx=True, no_channel_info=True,\n            **self._constrain_txo_from_kwargs(\n                {}, is_not_spent=True, is_my_output=True, **kwargs\n            )\n        )\n        txs = []\n        while txos:\n            txs.append(\n                await Transaction.create(\n                    [Input.spend(txos.pop()) for _ in range(min(len(txos), batch_size))],\n                    [], accounts, accounts[0]\n                )\n            )\n        if not preview:\n            for tx in txs:\n                await self.broadcast_or_release(tx, blocking)\n        if include_full_tx:\n            return txs\n        return [{'txid': tx.id} for tx in txs]\n\n    @requires(WALLET_COMPONENT)\n    def jsonrpc_txo_sum(self, account_id=None, wallet_id=None, **kwargs):\n        \"\"\"\n        Sum of transaction outputs.\n\n        Usage:\n            txo_list [--account_id=<account_id>] [--type=<type>...] [--txid=<txid>...]\n                     [--channel_id=<channel_id>...] [--not_channel_id=<not_channel_id>...]\n                     [--claim_id=<claim_id>...] [--name=<name>...]\n                     [--is_spent] [--is_not_spent]\n                     [--is_my_input_or_output |\n                         [[--is_my_output | --is_not_my_output] [--is_my_input | --is_not_my_input]]\n                     ]\n                     [--exclude_internal_transfers] [--wallet_id=<wallet_id>]\n\n        Options:\n            --type=<type>              : (str or list) claim type: stream, channel, support,\n                                         purchase, collection, repost, other\n            --txid=<txid>              : (str or list) transaction id of outputs\n            --claim_id=<claim_id>      : (str or list) claim id\n            --name=<name>              : (str or list) claim name\n            --channel_id=<channel_id>  : (str or list) claims in this channel\n      --not_channel_id=<not_channel_id>: (str or list) claims not in this channel\n            --is_spent                 : (bool) only show spent txos\n            --is_not_spent             : (bool) only show not spent txos\n            --is_my_input_or_output    : (bool) txos which have your inputs or your outputs,\n                                                if using this flag the other related flags\n                                                are ignored (--is_my_output, --is_my_input, etc)\n            --is_my_output             : (bool) show outputs controlled by you\n            --is_not_my_output         : (bool) show outputs not controlled by you\n            --is_my_input              : (bool) show outputs created by you\n            --is_not_my_input          : (bool) show outputs not created by you\n           --exclude_internal_transfers: (bool) excludes any outputs that are exactly this combination:\n                                                \"--is_my_input --is_my_output --type=other\"\n                                                this allows to exclude \"change\" payments, this\n                                                flag can be used in combination with any of the other flags\n            --account_id=<account_id>  : (str) id of the account to query\n            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet\n\n        Returns: int\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        return self.ledger.get_txo_sum(\n            wallet=wallet, accounts=[wallet.get_account_or_error(account_id)] if account_id else wallet.accounts,\n            read_only=True, **self._constrain_txo_from_kwargs({}, **kwargs)\n        )\n\n    @requires(WALLET_COMPONENT)\n    async def jsonrpc_txo_plot(\n            self, account_id=None, wallet_id=None,\n            days_back=0, start_day=None, days_after=None, end_day=None, **kwargs):\n        \"\"\"\n        Plot transaction output sum over days.\n\n        Usage:\n            txo_plot [--account_id=<account_id>] [--type=<type>...] [--txid=<txid>...]\n                     [--claim_id=<claim_id>...] [--name=<name>...] [--is_spent] [--is_not_spent]\n                     [--channel_id=<channel_id>...] [--not_channel_id=<not_channel_id>...]\n                     [--is_my_input_or_output |\n                         [[--is_my_output | --is_not_my_output] [--is_my_input | --is_not_my_input]]\n                     ]\n                     [--exclude_internal_transfers] [--wallet_id=<wallet_id>]\n                     [--days_back=<days_back> |\n                        [--start_day=<start_day> [--days_after=<days_after> | --end_day=<end_day>]]\n                     ]\n\n        Options:\n            --type=<type>              : (str or list) claim type: stream, channel, support,\n                                         purchase, collection, repost, other\n            --txid=<txid>              : (str or list) transaction id of outputs\n            --claim_id=<claim_id>      : (str or list) claim id\n            --name=<name>              : (str or list) claim name\n            --channel_id=<channel_id>  : (str or list) claims in this channel\n      --not_channel_id=<not_channel_id>: (str or list) claims not in this channel\n            --is_spent                 : (bool) only show spent txos\n            --is_not_spent             : (bool) only show not spent txos\n            --is_my_input_or_output    : (bool) txos which have your inputs or your outputs,\n                                                if using this flag the other related flags\n                                                are ignored (--is_my_output, --is_my_input, etc)\n            --is_my_output             : (bool) show outputs controlled by you\n            --is_not_my_output         : (bool) show outputs not controlled by you\n            --is_my_input              : (bool) show outputs created by you\n            --is_not_my_input          : (bool) show outputs not created by you\n           --exclude_internal_transfers: (bool) excludes any outputs that are exactly this combination:\n                                                \"--is_my_input --is_my_output --type=other\"\n                                                this allows to exclude \"change\" payments, this\n                                                flag can be used in combination with any of the other flags\n            --account_id=<account_id>  : (str) id of the account to query\n            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet\n            --days_back=<days_back>    : (int) number of days back from today\n                                               (not compatible with --start_day, --days_after, --end_day)\n            --start_day=<start_day>    : (date) start on specific date (YYYY-MM-DD)\n                                               (instead of --days_back)\n            --days_after=<days_after>  : (int) end number of days after --start_day\n                                               (instead of --end_day)\n            --end_day=<end_day>        : (date) end on specific date (YYYY-MM-DD)\n                                               (instead of --days_after)\n\n        Returns: List[Dict]\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        plot = await self.ledger.get_txo_plot(\n            wallet=wallet, accounts=[wallet.get_account_or_error(account_id)] if account_id else wallet.accounts,\n            read_only=True, days_back=days_back, start_day=start_day, days_after=days_after, end_day=end_day,\n            **self._constrain_txo_from_kwargs({}, **kwargs)\n        )\n        for row in plot:\n            row['total'] = dewies_to_lbc(row['total'])\n        return plot\n\n    UTXO_DOC = \"\"\"\n    Unspent transaction management.\n    \"\"\"\n\n    @requires(WALLET_COMPONENT)\n    def jsonrpc_utxo_list(self, *args, **kwargs):\n        \"\"\"\n        List unspent transaction outputs\n\n        Usage:\n            utxo_list [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>]\n                      [--page=<page>] [--page_size=<page_size>]\n\n        Options:\n            --account_id=<account_id>  : (str) id of the account to query\n            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet\n            --page=<page>              : (int) page to return during paginating\n            --page_size=<page_size>    : (int) number of items on page during pagination\n\n        Returns: {Paginated[Output]}\n        \"\"\"\n        kwargs['type'] = ['other', 'purchase']\n        kwargs['is_not_spent'] = True\n        return self.jsonrpc_txo_list(*args, **kwargs)\n\n    @requires(WALLET_COMPONENT)\n    async def jsonrpc_utxo_release(self, account_id=None, wallet_id=None):\n        \"\"\"\n        When spending a UTXO it is locally locked to prevent double spends;\n        occasionally this can result in a UTXO being locked which ultimately\n        did not get spent (failed to broadcast, spend transaction was not\n        accepted by blockchain node, etc). This command releases the lock\n        on all UTXOs in your account.\n\n        Usage:\n            utxo_release [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>]\n\n        Options:\n            --account_id=<account_id> : (str) id of the account to query\n            --wallet_id=<wallet_id>   : (str) restrict operation to specific wallet\n\n        Returns:\n            None\n        \"\"\"\n        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)\n        if account_id is not None:\n            await wallet.get_account_or_error(account_id).release_all_outputs()\n        else:\n            for account in wallet.accounts:\n                await account.release_all_outputs()\n\n    BLOB_DOC = \"\"\"\n    Blob management.\n    \"\"\"\n\n    @requires(WALLET_COMPONENT, DHT_COMPONENT, BLOB_COMPONENT)\n    async def jsonrpc_blob_get(self, blob_hash, timeout=None, read=False):\n        \"\"\"\n        Download and return a blob\n\n        Usage:\n            blob_get (<blob_hash> | --blob_hash=<blob_hash>) [--timeout=<timeout>] [--read]\n\n        Options:\n        --blob_hash=<blob_hash>                        : (str) blob hash of the blob to get\n        --timeout=<timeout>                            : (int) timeout in number of seconds\n\n        Returns:\n            (str) Success/Fail message or (dict) decoded data\n        \"\"\"\n\n        blob = await download_blob(asyncio.get_event_loop(), self.conf, self.blob_manager, self.dht_node, blob_hash)\n        if read:\n            with blob.reader_context() as handle:\n                return handle.read().decode()\n        elif isinstance(blob, BlobBuffer):\n            log.warning(\"manually downloaded blob buffer could have missed garbage collection, clearing it\")\n            blob.delete()\n        return \"Downloaded blob %s\" % blob_hash\n\n    @requires(BLOB_COMPONENT, DATABASE_COMPONENT)\n    async def jsonrpc_blob_delete(self, blob_hash):\n        \"\"\"\n        Delete a blob\n\n        Usage:\n            blob_delete (<blob_hash> | --blob_hash=<blob_hash>)\n\n        Options:\n            --blob_hash=<blob_hash>  : (str) blob hash of the blob to delete\n\n        Returns:\n            (str) Success/fail message\n        \"\"\"\n        if not blob_hash or not is_valid_blobhash(blob_hash):\n            return f\"Invalid blob hash to delete '{blob_hash}'\"\n        streams = self.file_manager.get_filtered(sd_hash=blob_hash)\n        if streams:\n            await self.file_manager.delete(streams[0])\n        else:\n            await self.blob_manager.delete_blobs([blob_hash])\n        return \"Deleted %s\" % blob_hash\n\n    PEER_DOC = \"\"\"\n    DHT / Blob Exchange peer commands.\n    \"\"\"\n\n    async def jsonrpc_peer_list(self, blob_hash, page=None, page_size=None):\n        \"\"\"\n        Get peers for blob hash\n\n        Usage:\n            peer_list (<blob_hash> | --blob_hash=<blob_hash>)\n                [--page=<page>] [--page_size=<page_size>]\n\n        Options:\n            --blob_hash=<blob_hash>                                  : (str) find available peers for this blob hash\n            --page=<page>                                            : (int) page to return during paginating\n            --page_size=<page_size>                                  : (int) number of items on page during pagination\n\n        Returns:\n            (list) List of contact dictionaries {'address': <peer ip>, 'udp_port': <dht port>, 'tcp_port': <peer port>,\n             'node_id': <peer node id>}\n        \"\"\"\n\n        if not is_valid_blobhash(blob_hash):\n            # TODO: use error from lbry.error\n            raise Exception(\"invalid blob hash\")\n        peer_q = asyncio.Queue(loop=self.component_manager.loop)\n        if self.component_manager.has_component(TRACKER_ANNOUNCER_COMPONENT):\n            tracker = self.component_manager.get_component(TRACKER_ANNOUNCER_COMPONENT)\n            tracker_peers = await tracker.get_kademlia_peer_list(bytes.fromhex(blob_hash))\n            log.info(\"Found %d peers for %s from trackers.\", len(tracker_peers), blob_hash[:8])\n            peer_q.put_nowait(tracker_peers)\n        elif not self.component_manager.has_component(DHT_COMPONENT):\n            raise Exception(\"Peer list needs, at least, either a DHT component or a Tracker component for discovery.\")\n        peers = []\n        if self.component_manager.has_component(DHT_COMPONENT):\n            await self.dht_node._peers_for_value_producer(blob_hash, peer_q)\n        while not peer_q.empty():\n            peers.extend(peer_q.get_nowait())\n        results = {\n            (peer.address, peer.tcp_port): {\n                \"node_id\": hexlify(peer.node_id).decode() if peer.node_id else None,\n                \"address\": peer.address,\n                \"udp_port\": peer.udp_port,\n                \"tcp_port\": peer.tcp_port,\n            }\n            for peer in peers\n        }\n        return paginate_list(list(results.values()), page, page_size)\n\n    @requires(DATABASE_COMPONENT)\n    async def jsonrpc_blob_announce(self, blob_hash=None, stream_hash=None, sd_hash=None):\n        \"\"\"\n        Announce blobs to the DHT\n\n        Usage:\n            blob_announce (<blob_hash> | --blob_hash=<blob_hash>\n                          | --stream_hash=<stream_hash> | --sd_hash=<sd_hash>)\n\n        Options:\n            --blob_hash=<blob_hash>        : (str) announce a blob, specified by blob_hash\n            --stream_hash=<stream_hash>    : (str) announce all blobs associated with\n                                             stream_hash\n            --sd_hash=<sd_hash>            : (str) announce all blobs associated with\n                                             sd_hash and the sd_hash itself\n\n        Returns:\n            (bool) true if successful\n        \"\"\"\n        blob_hashes = []\n        if blob_hash:\n            blob_hashes.append(blob_hash)\n        elif stream_hash or sd_hash:\n            if sd_hash and stream_hash:\n                # TODO: use error from lbry.error\n                raise Exception(\"either the sd hash or the stream hash should be provided, not both\")\n            if sd_hash:\n                stream_hash = await self.storage.get_stream_hash_for_sd_hash(sd_hash)\n            blobs = await self.storage.get_blobs_for_stream(stream_hash, only_completed=True)\n            blob_hashes.extend(blob.blob_hash for blob in blobs if blob.blob_hash is not None)\n        else:\n            # TODO: use error from lbry.error\n            raise Exception('single argument must be specified')\n        await self.storage.should_single_announce_blobs(blob_hashes, immediate=True)\n        return True\n\n    @requires(BLOB_COMPONENT, WALLET_COMPONENT)\n    async def jsonrpc_blob_list(self, uri=None, stream_hash=None, sd_hash=None, needed=None,\n                                finished=None, page=None, page_size=None):\n        \"\"\"\n        Returns blob hashes. If not given filters, returns all blobs known by the blob manager\n\n        Usage:\n            blob_list [--needed] [--finished] [<uri> | --uri=<uri>]\n                      [<stream_hash> | --stream_hash=<stream_hash>]\n                      [<sd_hash> | --sd_hash=<sd_hash>]\n                      [--page=<page>] [--page_size=<page_size>]\n\n        Options:\n            --needed                     : (bool) only return needed blobs\n            --finished                   : (bool) only return finished blobs\n            --uri=<uri>                  : (str) filter blobs by stream in a uri\n            --stream_hash=<stream_hash>  : (str) filter blobs by stream hash\n            --sd_hash=<sd_hash>          : (str) filter blobs in a stream by sd hash, ie the hash of the stream\n                                                 descriptor blob for a stream that has been downloaded\n            --page=<page>                : (int) page to return during paginating\n            --page_size=<page_size>      : (int) number of items on page during pagination\n\n        Returns:\n            (list) List of blob hashes\n        \"\"\"\n\n        if uri or stream_hash or sd_hash:\n            if uri:\n                metadata = (await self.resolve([], uri))[uri]\n                sd_hash = utils.get_sd_hash(metadata)\n                stream_hash = await self.storage.get_stream_hash_for_sd_hash(sd_hash)\n            elif stream_hash:\n                sd_hash = await self.storage.get_sd_blob_hash_for_stream(stream_hash)\n            elif sd_hash:\n                stream_hash = await self.storage.get_stream_hash_for_sd_hash(sd_hash)\n                sd_hash = await self.storage.get_sd_blob_hash_for_stream(stream_hash)\n            if sd_hash:\n                blobs = [sd_hash]\n            else:\n                blobs = []\n            if stream_hash:\n                blobs.extend([b.blob_hash for b in (await self.storage.get_blobs_for_stream(stream_hash))[:-1]])\n        else:\n            blobs = list(self.blob_manager.completed_blob_hashes)\n        if needed:\n            blobs = [blob_hash for blob_hash in blobs if not self.blob_manager.is_blob_verified(blob_hash)]\n        if finished:\n            blobs = [blob_hash for blob_hash in blobs if self.blob_manager.is_blob_verified(blob_hash)]\n        return paginate_list(blobs, page, page_size)\n\n    @requires(BLOB_COMPONENT)\n    async def jsonrpc_blob_reflect(self, blob_hashes, reflector_server=None):\n        \"\"\"\n        Reflects specified blobs\n\n        Usage:\n            blob_reflect (<blob_hashes>...) [--reflector_server=<reflector_server>]\n\n        Options:\n            --reflector_server=<reflector_server>          : (str) reflector address\n\n        Returns:\n            (list) reflected blob hashes\n        \"\"\"\n\n        raise NotImplementedError()\n\n    @requires(BLOB_COMPONENT)\n    async def jsonrpc_blob_reflect_all(self):\n        \"\"\"\n        Reflects all saved blobs\n\n        Usage:\n            blob_reflect_all\n\n        Options:\n            None\n\n        Returns:\n            (bool) true if successful\n        \"\"\"\n\n        raise NotImplementedError()\n\n    @requires(DISK_SPACE_COMPONENT)\n    async def jsonrpc_blob_clean(self):\n        \"\"\"\n        Deletes blobs to cleanup disk space\n\n        Usage:\n            blob_clean\n\n        Options:\n            None\n\n        Returns:\n            (bool) true if successful\n        \"\"\"\n        return await self.disk_space_manager.clean()\n\n    @requires(FILE_MANAGER_COMPONENT)\n    async def jsonrpc_file_reflect(self, **kwargs):\n        \"\"\"\n        Reflect all the blobs in a file matching the filter criteria\n\n        Usage:\n            file_reflect [--sd_hash=<sd_hash>] [--file_name=<file_name>]\n                         [--stream_hash=<stream_hash>] [--rowid=<rowid>]\n                         [--reflector=<reflector>]\n\n        Options:\n            --sd_hash=<sd_hash>          : (str) get file with matching sd hash\n            --file_name=<file_name>      : (str) get file with matching file name in the\n                                           downloads folder\n            --stream_hash=<stream_hash>  : (str) get file with matching stream hash\n            --rowid=<rowid>              : (int) get file with matching row id\n            --reflector=<reflector>      : (str) reflector server, ip address or url\n                                           by default choose a server from the config\n\n        Returns:\n            (list) list of blobs reflected\n        \"\"\"\n\n        server, port = kwargs.get('server'), kwargs.get('port')\n        if server and port:\n            port = int(port)\n        else:\n            server, port = random.choice(self.conf.reflector_servers)\n        reflected = await asyncio.gather(*[\n            self.file_manager.source_managers['stream'].reflect_stream(stream, server, port)\n            for stream in self.file_manager.get_filtered(**kwargs)\n        ])\n        total = []\n        for reflected_for_stream in reflected:\n            total.extend(reflected_for_stream)\n        return total\n\n    @requires(DHT_COMPONENT)\n    async def jsonrpc_peer_ping(self, node_id, address, port):\n        \"\"\"\n        Send a kademlia ping to the specified peer. If address and port are provided the peer is directly pinged,\n        if not provided the peer is located first.\n\n        Usage:\n            peer_ping (<node_id> | --node_id=<node_id>) (<address> | --address=<address>) (<port> | --port=<port>)\n\n        Options:\n            None\n\n        Returns:\n            (str) pong, or {'error': <error message>} if an error is encountered\n        \"\"\"\n        peer = None\n        if node_id and address and port:\n            peer = make_kademlia_peer(unhexlify(node_id), address, udp_port=int(port))\n            try:\n                return await self.dht_node.protocol.get_rpc_peer(peer).ping()\n            except asyncio.TimeoutError:\n                return {'error': 'timeout'}\n        if not peer:\n            return {'error': 'peer not found'}\n\n    @requires(DHT_COMPONENT)\n    def jsonrpc_routing_table_get(self):\n        \"\"\"\n        Get DHT routing information\n\n        Usage:\n            routing_table_get\n\n        Options:\n            None\n\n        Returns:\n            (dict) dictionary containing routing and peer information\n            {\n                \"buckets\": {\n                    <bucket index>: [\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            }\n        \"\"\"\n        result = {\n            'buckets': {},\n            'prefix_neighbors_count': 0\n        }\n\n        for i, _ in enumerate(self.dht_node.protocol.routing_table.buckets):\n            result['buckets'][i] = []\n            for peer in self.dht_node.protocol.routing_table.buckets[i].peers:\n                host = {\n                    \"address\": peer.address,\n                    \"udp_port\": peer.udp_port,\n                    \"tcp_port\": peer.tcp_port,\n                    \"node_id\": hexlify(peer.node_id).decode(),\n                }\n                result['buckets'][i].append(host)\n                result['prefix_neighbors_count'] += 1 if peer.node_id[0] == self.dht_node.protocol.node_id[0] else 0\n\n        result['node_id'] = hexlify(self.dht_node.protocol.node_id).decode()\n        return result\n\n    TRACEMALLOC_DOC = \"\"\"\n    Controls and queries tracemalloc memory tracing tools for troubleshooting.\n    \"\"\"\n\n    def jsonrpc_tracemalloc_enable(self):  # pylint: disable=no-self-use\n        \"\"\"\n        Enable tracemalloc memory tracing\n\n        Usage:\n            jsonrpc_tracemalloc_enable\n\n        Options:\n            None\n\n        Returns:\n            (bool) is it tracing?\n        \"\"\"\n        tracemalloc.start()\n        return tracemalloc.is_tracing()\n\n    def jsonrpc_tracemalloc_disable(self):  # pylint: disable=no-self-use\n        \"\"\"\n        Disable tracemalloc memory tracing\n\n        Usage:\n            jsonrpc_tracemalloc_disable\n\n        Options:\n            None\n\n        Returns:\n            (bool) is it tracing?\n        \"\"\"\n        tracemalloc.stop()\n        return tracemalloc.is_tracing()\n\n    def jsonrpc_tracemalloc_top(self, items: int = 10):  # pylint: disable=no-self-use\n        \"\"\"\n        Show most common objects, the place that created them and their size.\n\n        Usage:\n            jsonrpc_tracemalloc_top [(<items> | --items=<items>)]\n\n        Options:\n            --items=<items>               : (int) maximum items to return, from the most common\n\n        Returns:\n            (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            }\n        \"\"\"\n        if not tracemalloc.is_tracing():\n            # TODO: use error from lbry.error\n            raise Exception(\"Enable tracemalloc first! See 'tracemalloc set' command.\")\n        stats = tracemalloc.take_snapshot().filter_traces((\n            tracemalloc.Filter(False, \"<frozen importlib._bootstrap>\"),\n            tracemalloc.Filter(False, \"<unknown>\"),\n            # tracemalloc and linecache here use some memory, but thats not relevant\n            tracemalloc.Filter(False, tracemalloc.__file__),\n            tracemalloc.Filter(False, linecache.__file__),\n        )).statistics('lineno', True)\n        results = []\n        for stat in stats:\n            frame = stat.traceback[0]\n            filename = os.sep.join(frame.filename.split(os.sep)[-2:])\n            line = linecache.getline(frame.filename, frame.lineno).strip()\n            results.append({\n                \"line\": f\"{filename}:{frame.lineno}\",\n                \"code\": line,\n                \"size\": stat.size,\n                \"count\": stat.count\n            })\n            if len(results) == items:\n                break\n        return results\n\n    async def broadcast_or_release(self, tx, blocking=False):\n        await self.wallet_manager.broadcast_or_release(tx, blocking)\n\n    def valid_address_or_error(self, address, allow_script_address=False):\n        try:\n            assert self.ledger.is_pubkey_address(address) or (\n                allow_script_address and self.ledger.is_script_address(address)\n            )\n        except:\n            # TODO: use error from lbry.error\n            raise Exception(f\"'{address}' is not a valid address\")\n\n    @staticmethod\n    def valid_stream_name_or_error(name: str):\n        try:\n            if not name:\n                raise InputStringIsBlankError('Stream name')\n            parsed = URL.parse(name)\n            if parsed.has_channel:\n                # TODO: use error from lbry.error\n                raise Exception(\n                    \"Stream names cannot start with '@' symbol. This is reserved for channels claims.\"\n                )\n            if not parsed.has_stream or parsed.stream.name != name:\n                # TODO: use error from lbry.error\n                raise Exception('Stream name has invalid characters.')\n        except (TypeError, ValueError):\n            # TODO: use error from lbry.error\n            raise Exception(\"Invalid stream name.\")\n\n    @staticmethod\n    def valid_collection_name_or_error(name: str):\n        try:\n            if not name:\n                # TODO: use error from lbry.error\n                raise Exception('Collection name cannot be blank.')\n            parsed = URL.parse(name)\n            if parsed.has_channel:\n                # TODO: use error from lbry.error\n                raise Exception(\n                    \"Collection names cannot start with '@' symbol. This is reserved for channels claims.\"\n                )\n            if not parsed.has_stream or parsed.stream.name != name:\n                # TODO: use error from lbry.error\n                raise Exception('Collection name has invalid characters.')\n        except (TypeError, ValueError):\n            # TODO: use error from lbry.error\n            raise Exception(\"Invalid collection name.\")\n\n    @staticmethod\n    def valid_channel_name_or_error(name: str):\n        try:\n            if not name:\n                # TODO: use error from lbry.error\n                raise Exception(\n                    \"Channel name cannot be blank.\"\n                )\n            parsed = URL.parse(name)\n            if not parsed.has_channel:\n                # TODO: use error from lbry.error\n                raise Exception(\"Channel names must start with '@' symbol.\")\n            if parsed.channel.name != name:\n                # TODO: use error from lbry.error\n                raise Exception(\"Channel name has invalid character\")\n        except (TypeError, ValueError):\n            # TODO: use error from lbry.error\n            raise Exception(\"Invalid channel name.\")\n\n    def get_fee_address(self, kwargs: dict, claim_address: str) -> str:\n        if 'fee_address' in kwargs:\n            self.valid_address_or_error(kwargs['fee_address'])\n            return kwargs['fee_address']\n        if 'fee_currency' in kwargs or 'fee_amount' in kwargs:\n            return claim_address\n\n    async def get_receiving_address(self, address: str, account: Optional[Account]) -> str:\n        if address is None and account is not None:\n            return await account.receiving.get_or_create_usable_address()\n        self.valid_address_or_error(address)\n        return address\n\n    async def get_channel_or_none(\n            self, wallet: Wallet, account_ids: List[str], channel_id: str = None,\n            channel_name: str = None, for_signing: bool = False) -> Output:\n        if channel_id is not None or channel_name is not None:\n            return await self.get_channel_or_error(\n                wallet, account_ids, channel_id, channel_name, for_signing\n            )\n\n    async def get_channel_or_error(\n            self, wallet: Wallet, account_ids: List[str], channel_id: str = None,\n            channel_name: str = None, for_signing: bool = False) -> Output:\n        if channel_id:\n            key, value = 'id', channel_id\n        elif channel_name:\n            key, value = 'name', channel_name\n        else:\n            # TODO: use error from lbry.error\n            raise ValueError(\"Couldn't find channel because a channel_id or channel_name was not provided.\")\n        channels = await self.ledger.get_channels(\n            wallet=wallet, accounts=wallet.get_accounts_or_all(account_ids),\n            **{f'claim_{key}': value}\n        )\n        if len(channels) == 1:\n            if for_signing and not channels[0].has_private_key:\n                # TODO: use error from lbry.error\n                raise PrivateKeyNotFoundError(key, value)\n            return channels[0]\n        elif len(channels) > 1:\n            # TODO: use error from lbry.error\n            raise ValueError(\n                f\"Multiple channels found with channel_{key} '{value}', \"\n                f\"pass a channel_id to narrow it down.\"\n            )\n        # TODO: use error from lbry.error\n        raise ValueError(f\"Couldn't find channel with channel_{key} '{value}'.\")\n\n    @staticmethod\n    def get_dewies_or_error(argument: str, lbc: str, positive_value=False):\n        try:\n            dewies = lbc_to_dewies(lbc)\n            if positive_value and dewies <= 0:\n                # TODO: use error from lbry.error\n                raise ValueError(f\"'{argument}' value must be greater than 0.0\")\n            return dewies\n        except ValueError as e:\n            # TODO: use error from lbry.error\n            raise ValueError(f\"Invalid value for '{argument}': {e.args[0]}\")\n\n    async def resolve(self, accounts, urls, **kwargs):\n        results = await self.ledger.resolve(accounts, urls, **kwargs)\n        if self.conf.save_resolved_claims and results:\n            try:\n                await self.storage.save_claim_from_output(\n                    self.ledger,\n                    *(result for result in results.values() if isinstance(result, Output))\n                )\n            except DecodeError:\n                pass\n        return results\n\n    @staticmethod\n    def _old_get_temp_claim_info(tx, txo, address, claim_dict, name):\n        return {\n            \"claim_id\": txo.claim_id,\n            \"name\": name,\n            \"amount\": dewies_to_lbc(txo.amount),\n            \"address\": address,\n            \"txid\": tx.id,\n            \"nout\": txo.position,\n            \"value\": claim_dict,\n            \"height\": -1,\n            \"claim_sequence\": -1,\n        }\n\n\ndef loggly_time_string(date):\n    formatted_dt = date.strftime(\"%Y-%m-%dT%H:%M:%S\")\n    milliseconds = str(round(date.microsecond * (10.0 ** -5), 3))\n    return quote(formatted_dt + milliseconds + \"Z\")\n\n\ndef get_loggly_query_string(installation_id):\n    base_loggly_search_url = \"https://lbry.loggly.com/search#\"\n    now = utils.now()\n    yesterday = now - utils.timedelta(days=1)\n    params = {\n        'terms': f'json.installation_id:{installation_id[:SHORT_ID_LEN]}*',\n        'from': loggly_time_string(yesterday),\n        'to': loggly_time_string(now)\n    }\n    data = urlencode(params)\n    return base_loggly_search_url + data\n"
  },
  {
    "path": "lbry/extras/daemon/exchange_rate_manager.py",
    "content": "import json\nimport time\nimport asyncio\nimport logging\nfrom statistics import median\nfrom decimal import Decimal\nfrom typing import Optional, Iterable, Type\nfrom aiohttp.client_exceptions import ContentTypeError, ClientConnectionError\nfrom lbry.error import InvalidExchangeRateResponseError, CurrencyConversionError\nfrom lbry.utils import aiohttp_request\nfrom lbry.wallet.dewies import lbc_to_dewies\n\nlog = logging.getLogger(__name__)\n\n\nclass ExchangeRate:\n    def __init__(self, market, spot, ts):\n        if not int(time.time()) - ts < 600:\n            raise ValueError('The timestamp is too dated.')\n        if not spot > 0:\n            raise ValueError('Spot must be greater than 0.')\n        self.currency_pair = (market[0:3], market[3:6])\n        self.spot = spot\n        self.ts = ts\n\n    def __repr__(self):\n        return f\"Currency pair:{self.currency_pair}, spot:{self.spot}, ts:{self.ts}\"\n\n    def as_dict(self):\n        return {'spot': self.spot, 'ts': self.ts}\n\n\nclass MarketFeed:\n    name: str = \"\"\n    market: str = \"\"\n    url: str = \"\"\n    params = {}\n    fee = 0\n\n    update_interval = 300\n    request_timeout = 50\n\n    def __init__(self):\n        self.rate: Optional[float] = None\n        self.last_check = 0\n        self._last_response = None\n        self._task: Optional[asyncio.Task] = None\n        self.event = asyncio.Event()\n\n    @property\n    def has_rate(self):\n        return self.rate is not None\n\n    @property\n    def is_online(self):\n        return self.last_check+self.update_interval+self.request_timeout > time.time()\n\n    def get_rate_from_response(self, json_response):\n        raise NotImplementedError()\n\n    async def get_response(self):\n        async with aiohttp_request(\n                'get', self.url, params=self.params,\n                timeout=self.request_timeout, headers={\"User-Agent\": \"lbrynet\"}\n        ) as response:\n            try:\n                self._last_response = await response.json(content_type=None)\n            except ContentTypeError as e:\n                self._last_response = {}\n                log.warning(\"Could not parse exchange rate response from %s: %s\", self.name, e.message)\n                log.debug(await response.text())\n            return self._last_response\n\n    async def get_rate(self):\n        try:\n            data = await self.get_response()\n            rate = self.get_rate_from_response(data)\n            rate = rate / (1.0 - self.fee)\n            log.debug(\"Saving rate update %f for %s from %s\", rate, self.market, self.name)\n            self.rate = ExchangeRate(self.market, rate, int(time.time()))\n            self.last_check = time.time()\n            return self.rate\n        except asyncio.TimeoutError:\n            log.warning(\"Timed out fetching exchange rate from %s.\", self.name)\n        except json.JSONDecodeError as e:\n            msg = e.doc if '<html>' not in e.doc else 'unexpected content type.'\n            log.warning(\"Could not parse exchange rate response from %s: %s\", self.name, msg)\n            log.debug(e.doc)\n        except InvalidExchangeRateResponseError as e:\n            log.warning(str(e))\n        except ClientConnectionError as e:\n            log.warning(\"Error trying to connect to exchange rate %s: %s\", self.name, str(e))\n        except Exception as e:\n            log.exception(\"Exchange rate error (%s from %s):\", self.market, self.name)\n        finally:\n            self.event.set()\n\n    async def keep_updated(self):\n        while True:\n            await self.get_rate()\n            await asyncio.sleep(self.update_interval)\n\n    def start(self):\n        if not self._task:\n            self._task = asyncio.create_task(self.keep_updated())\n\n    def stop(self):\n        if self._task and not self._task.done():\n            self._task.cancel()\n        self._task = None\n        self.event.clear()\n\n\nclass BaseBittrexFeed(MarketFeed):\n    name = \"Bittrex\"\n    market = None\n    url = None\n    fee = 0.0025\n\n    def get_rate_from_response(self, json_response):\n        if 'lastTradeRate' not in json_response:\n            raise InvalidExchangeRateResponseError(self.name, 'result not found')\n        return 1.0 / float(json_response['lastTradeRate'])\n\n\nclass BittrexBTCFeed(BaseBittrexFeed):\n    market = \"BTCLBC\"\n    url = \"https://api.bittrex.com/v3/markets/LBC-BTC/ticker\"\n\n\nclass BittrexUSDFeed(BaseBittrexFeed):\n    market = \"USDLBC\"\n    url = \"https://api.bittrex.com/v3/markets/LBC-USD/ticker\"\n\n\nclass BaseCoinExFeed(MarketFeed):\n    name = \"CoinEx\"\n    market = None\n    url = None\n\n    def get_rate_from_response(self, json_response):\n        if 'data' not in json_response or \\\n           'ticker' not in json_response['data'] or \\\n           'last' not in json_response['data']['ticker']:\n            raise InvalidExchangeRateResponseError(self.name, 'result not found')\n        return 1.0 / float(json_response['data']['ticker']['last'])\n\n\nclass CoinExBTCFeed(BaseCoinExFeed):\n    market = \"BTCLBC\"\n    url = \"https://api.coinex.com/v1/market/ticker?market=LBCBTC\"\n\n\nclass CoinExUSDFeed(BaseCoinExFeed):\n    market = \"USDLBC\"\n    url = \"https://api.coinex.com/v1/market/ticker?market=LBCUSDT\"\n\n\nclass BaseHotbitFeed(MarketFeed):\n    name = \"hotbit\"\n    market = None\n    url = \"https://api.hotbit.io/api/v1/market.last\"\n\n    def get_rate_from_response(self, json_response):\n        if 'result' not in json_response:\n            raise InvalidExchangeRateResponseError(self.name, 'result not found')\n        return 1.0 / float(json_response['result'])\n\n\nclass HotbitBTCFeed(BaseHotbitFeed):\n    market = \"BTCLBC\"\n    params = {\"market\": \"LBC/BTC\"}\n\n\nclass HotbitUSDFeed(BaseHotbitFeed):\n    market = \"USDLBC\"\n    params = {\"market\": \"LBC/USDT\"}\n\n\nclass UPbitBTCFeed(MarketFeed):\n    name = \"UPbit\"\n    market = \"BTCLBC\"\n    url = \"https://api.upbit.com/v1/ticker\"\n    params = {\"markets\": \"BTC-LBC\"}\n\n    def get_rate_from_response(self, json_response):\n        if \"error\" in json_response or len(json_response) != 1 or 'trade_price' not in json_response[0]:\n            raise InvalidExchangeRateResponseError(self.name, 'result not found')\n        return 1.0 / float(json_response[0]['trade_price'])\n\n\nFEEDS: Iterable[Type[MarketFeed]] = (\n    BittrexBTCFeed,\n    BittrexUSDFeed,\n    CoinExBTCFeed,\n    CoinExUSDFeed,\n#    HotbitBTCFeed,\n#    HotbitUSDFeed,\n#    UPbitBTCFeed,\n)\n\n\nclass ExchangeRateManager:\n    def __init__(self, feeds=FEEDS):\n        self.market_feeds = [Feed() for Feed in feeds]\n\n    def wait(self):\n        return asyncio.wait(\n            [feed.event.wait() for feed in self.market_feeds],\n        )\n\n    def start(self):\n        log.info(\"Starting exchange rate manager\")\n        for feed in self.market_feeds:\n            feed.start()\n\n    def stop(self):\n        log.info(\"Stopping exchange rate manager\")\n        for source in self.market_feeds:\n            source.stop()\n\n    def convert_currency(self, from_currency, to_currency, amount):\n        log.debug(\n            \"Converting %f %s to %s, rates: %s\",\n            amount, from_currency, to_currency,\n            [market.rate for market in self.market_feeds]\n        )\n        if from_currency == to_currency:\n            return round(amount, 8)\n\n        rates = []\n        for market in self.market_feeds:\n            if (market.has_rate and market.is_online and\n                    market.rate.currency_pair == (from_currency, to_currency)):\n                rates.append(market.rate.spot)\n\n        if rates:\n            return round(amount * Decimal(median(rates)), 8)\n\n        raise CurrencyConversionError(\n            f'Unable to convert {amount} from {from_currency} to {to_currency}')\n\n    def to_dewies(self, currency, amount) -> int:\n        converted = self.convert_currency(currency, \"LBC\", amount)\n        return lbc_to_dewies(str(converted))\n\n    def fee_dict(self):\n        return {market: market.rate.as_dict() for market in self.market_feeds}\n"
  },
  {
    "path": "lbry/extras/daemon/json_response_encoder.py",
    "content": "import logging\nfrom decimal import Decimal\nfrom binascii import hexlify, unhexlify\nfrom datetime import datetime\nfrom json import JSONEncoder\n\nfrom google.protobuf.message import DecodeError\n\nfrom lbry.schema.claim import Claim\nfrom lbry.schema.support import Support\nfrom lbry.torrent.torrent_manager import TorrentSource\nfrom lbry.wallet import Wallet, Ledger, Account, Transaction, Output\nfrom lbry.wallet.bip32 import PublicKey\nfrom lbry.wallet.dewies import dewies_to_lbc\nfrom lbry.stream.managed_stream import ManagedStream\n\n\nlog = logging.getLogger(__name__)\n\n\ndef encode_txo_doc():\n    return {\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\ndef encode_tx_doc():\n    return {\n        'txid': \"hash of transaction in hex\",\n        'height': \"block where transaction was recorded\",\n        'inputs': [encode_txo_doc()],\n        'outputs': [encode_txo_doc()],\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\n\ndef encode_account_doc():\n    return {\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\ndef encode_wallet_doc():\n    return {\n        'id': 'wallet_id',\n        'name': 'optional wallet name',\n    }\n\n\ndef encode_file_doc():\n    return {\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\nclass JSONResponseEncoder(JSONEncoder):\n\n    def __init__(self, *args, ledger: Ledger, include_protobuf=False, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.ledger = ledger\n        self.include_protobuf = include_protobuf\n\n    def default(self, obj):  # pylint: disable=method-hidden,arguments-renamed,too-many-return-statements\n        if isinstance(obj, Account):\n            return self.encode_account(obj)\n        if isinstance(obj, Wallet):\n            return self.encode_wallet(obj)\n        if isinstance(obj, (ManagedStream, TorrentSource)):\n            return self.encode_file(obj)\n        if isinstance(obj, Transaction):\n            return self.encode_transaction(obj)\n        if isinstance(obj, Output):\n            return self.encode_output(obj)\n        if isinstance(obj, Claim):\n            return self.encode_claim(obj)\n        if isinstance(obj, Support):\n            return obj.to_dict()\n        if isinstance(obj, PublicKey):\n            return obj.extended_key_string()\n        if isinstance(obj, datetime):\n            return obj.strftime(\"%Y%m%dT%H:%M:%S\")\n        if isinstance(obj, Decimal):\n            return float(obj)\n        if isinstance(obj, bytes):\n            return obj.decode()\n        return super().default(obj)\n\n    def encode_transaction(self, tx):\n        return {\n            'txid': tx.id,\n            'height': tx.height,\n            'inputs': [self.encode_input(txo) for txo in tx.inputs],\n            'outputs': [self.encode_output(txo) for txo in tx.outputs],\n            'total_input': dewies_to_lbc(tx.input_sum),\n            'total_output': dewies_to_lbc(tx.input_sum - tx.fee),\n            'total_fee': dewies_to_lbc(tx.fee),\n            'hex': hexlify(tx.raw).decode(),\n        }\n\n    def encode_output(self, txo, check_signature=True):\n        if not txo:\n            return\n        tx_height = txo.tx_ref.height\n        best_height = self.ledger.headers.height\n        output = {\n            'txid': txo.tx_ref.id,\n            'nout': txo.position,\n            'height': tx_height,\n            'amount': dewies_to_lbc(txo.amount),\n            'address': txo.get_address(self.ledger) if txo.has_address else None,\n            'confirmations': (best_height+1) - tx_height if tx_height > 0 else tx_height,\n            'timestamp': self.ledger.headers.estimated_timestamp(tx_height)\n        }\n        if txo.is_spent is not None:\n            output['is_spent'] = txo.is_spent\n        if txo.is_my_output is not None:\n            output['is_my_output'] = txo.is_my_output\n        if txo.is_my_input is not None:\n            output['is_my_input'] = txo.is_my_input\n        if txo.sent_supports is not None:\n            output['sent_supports'] = dewies_to_lbc(txo.sent_supports)\n        if txo.sent_tips is not None:\n            output['sent_tips'] = dewies_to_lbc(txo.sent_tips)\n        if txo.received_tips is not None:\n            output['received_tips'] = dewies_to_lbc(txo.received_tips)\n        if txo.is_internal_transfer is not None:\n            output['is_internal_transfer'] = txo.is_internal_transfer\n\n        if txo.script.is_claim_name:\n            output['type'] = 'claim'\n            output['claim_op'] = 'create'\n        elif txo.script.is_update_claim:\n            output['type'] = 'claim'\n            output['claim_op'] = 'update'\n        elif txo.script.is_support_claim:\n            output['type'] = 'support'\n        elif txo.script.is_return_data:\n            output['type'] = 'data'\n        elif txo.purchase is not None:\n            output['type'] = 'purchase'\n            output['claim_id'] = txo.purchased_claim_id\n            if txo.purchased_claim is not None:\n                output['claim'] = self.encode_output(txo.purchased_claim)\n        else:\n            output['type'] = 'payment'\n\n        if txo.script.is_claim_involved:\n            output.update({\n                'name': txo.claim_name,\n                'normalized_name': txo.normalized_name,\n                'claim_id': txo.claim_id,\n                'permanent_url': txo.permanent_url,\n                'meta': self.encode_claim_meta(txo.meta.copy())\n            })\n            if 'short_url' in output['meta']:\n                output['short_url'] = output['meta'].pop('short_url')\n            if 'canonical_url' in output['meta']:\n                output['canonical_url'] = output['meta'].pop('canonical_url')\n            if txo.claims is not None:\n                output['claims'] = [self.encode_output(o) for o in txo.claims]\n            if txo.reposted_claim is not None:\n                output['reposted_claim'] = self.encode_output(txo.reposted_claim)\n        if txo.script.is_claim_name or txo.script.is_update_claim or txo.script.is_support_claim_data:\n            try:\n                output['value'] = txo.signable\n                if self.include_protobuf:\n                    output['protobuf'] = hexlify(txo.signable.to_bytes())\n                if txo.purchase_receipt is not None:\n                    output['purchase_receipt'] = self.encode_output(txo.purchase_receipt)\n                if txo.script.is_claim_name or txo.script.is_update_claim:\n                    output['value_type'] = txo.claim.claim_type\n                    if txo.claim.is_channel:\n                        output['has_signing_key'] = txo.has_private_key\n                if check_signature and txo.signable.is_signed:\n                    if txo.channel is not None:\n                        output['signing_channel'] = self.encode_output(txo.channel)\n                        output['is_channel_signature_valid'] = txo.is_signed_by(txo.channel, self.ledger)\n                    else:\n                        output['signing_channel'] = {'channel_id': txo.signable.signing_channel_id}\n                        output['is_channel_signature_valid'] = False\n            except DecodeError:\n                pass\n        return output\n\n    def encode_claim_meta(self, meta):\n        for key, value in meta.items():\n            if key.endswith('_amount'):\n                if isinstance(value, int):\n                    meta[key] = dewies_to_lbc(value)\n        if 0 < meta.get('creation_height', 0) <= self.ledger.headers.height:\n            meta['creation_timestamp'] = self.ledger.headers.estimated_timestamp(meta['creation_height'])\n        return meta\n\n    def encode_input(self, txi):\n        return self.encode_output(txi.txo_ref.txo, False) if txi.txo_ref.txo is not None else {\n            'txid': txi.txo_ref.tx_ref.id,\n            'nout': txi.txo_ref.position\n        }\n\n    def encode_account(self, account):\n        result = account.to_dict()\n        result['id'] = account.id\n        result.pop('certificates', None)\n        result['is_default'] = self.ledger.accounts[0] == account\n        return result\n\n    @staticmethod\n    def encode_wallet(wallet):\n        return {\n            'id': wallet.id,\n            'name': wallet.name\n        }\n\n    def encode_file(self, managed_stream):\n        output_exists = managed_stream.output_file_exists\n        tx_height = managed_stream.stream_claim_info.height\n        best_height = self.ledger.headers.height\n        is_stream = hasattr(managed_stream, 'stream_hash')\n        if is_stream:\n            total_bytes_lower_bound = managed_stream.descriptor.lower_bound_decrypted_length()\n            total_bytes = managed_stream.descriptor.upper_bound_decrypted_length()\n        else:\n            total_bytes_lower_bound = total_bytes = managed_stream.torrent_length\n        result = {\n            'streaming_url': None,\n            'completed': managed_stream.completed,\n            'file_name': None,\n            'download_directory': None,\n            'download_path': None,\n            'points_paid': 0.0,\n            'stopped': not managed_stream.running,\n            'stream_hash': None,\n            'stream_name': None,\n            'suggested_file_name': None,\n            'sd_hash': None,\n            'mime_type': None,\n            'key': None,\n            'total_bytes_lower_bound': total_bytes_lower_bound,\n            'total_bytes': total_bytes,\n            'written_bytes': managed_stream.written_bytes,\n            'blobs_completed': None,\n            'blobs_in_stream': None,\n            'blobs_remaining': None,\n            'status': managed_stream.status,\n            'claim_id': managed_stream.claim_id,\n            'txid': managed_stream.txid,\n            'nout': managed_stream.nout,\n            'outpoint': managed_stream.outpoint,\n            'metadata': managed_stream.metadata,\n            'protobuf': managed_stream.metadata_protobuf,\n            'channel_claim_id': managed_stream.channel_claim_id,\n            'channel_name': managed_stream.channel_name,\n            'claim_name': managed_stream.claim_name,\n            'content_fee': managed_stream.content_fee,\n            'purchase_receipt': self.encode_output(managed_stream.purchase_receipt),\n            'added_on': managed_stream.added_on,\n            'height': tx_height,\n            'confirmations': (best_height + 1) - tx_height if tx_height > 0 else tx_height,\n            'timestamp': self.ledger.headers.estimated_timestamp(tx_height),\n            'is_fully_reflected': False,\n            'reflector_progress': False,\n            'uploading_to_reflector': False\n        }\n        if is_stream:\n            result.update({\n                'streaming_url': managed_stream.stream_url,\n                'stream_hash': managed_stream.stream_hash,\n                'stream_name': managed_stream.stream_name,\n                'suggested_file_name': managed_stream.suggested_file_name,\n                'sd_hash': managed_stream.descriptor.sd_hash,\n                'mime_type': managed_stream.mime_type,\n                'key': managed_stream.descriptor.key,\n                'blobs_completed': managed_stream.blobs_completed,\n                'blobs_in_stream': managed_stream.blobs_in_stream,\n                'blobs_remaining': managed_stream.blobs_remaining,\n                'is_fully_reflected': managed_stream.is_fully_reflected,\n                'reflector_progress': managed_stream.reflector_progress,\n                'uploading_to_reflector': managed_stream.uploading_to_reflector\n            })\n        else:\n            result.update({\n                'streaming_url': f'file://{managed_stream.full_path}',\n            })\n        if output_exists:\n            result.update({\n                'file_name': managed_stream.file_name,\n                'download_directory': managed_stream.download_directory,\n                'download_path': managed_stream.full_path,\n            })\n        return result\n\n    def encode_claim(self, claim):\n        encoded = getattr(claim, claim.claim_type).to_dict()\n        if 'public_key' in encoded:\n            encoded['public_key_id'] = self.ledger.public_key_to_address(\n                unhexlify(encoded['public_key'])\n            )\n        return encoded\n"
  },
  {
    "path": "lbry/extras/daemon/migrator/__init__.py",
    "content": ""
  },
  {
    "path": "lbry/extras/daemon/migrator/dbmigrator.py",
    "content": "# pylint: skip-file\nimport os\nimport sys\nimport logging\n\nlog = logging.getLogger(__name__)\n\n\ndef migrate_db(conf, start, end):\n    current = start\n    while current < end:\n        if current == 1:\n            from .migrate1to2 import do_migration\n        elif current == 2:\n            from .migrate2to3 import do_migration\n        elif current == 3:\n            from .migrate3to4 import do_migration\n        elif current == 4:\n            from .migrate4to5 import do_migration\n        elif current == 5:\n            from .migrate5to6 import do_migration\n        elif current == 6:\n            from .migrate6to7 import do_migration\n        elif current == 7:\n            from .migrate7to8 import do_migration\n        elif current == 8:\n            from .migrate8to9 import do_migration\n        elif current == 9:\n            from .migrate9to10 import do_migration\n        elif current == 10:\n            from .migrate10to11 import do_migration\n        elif current == 11:\n            from .migrate11to12 import do_migration\n        elif current == 12:\n            from .migrate12to13 import do_migration\n        elif current == 13:\n            from .migrate13to14 import do_migration\n        elif current == 14:\n            from .migrate14to15 import do_migration\n        elif current == 15:\n            from .migrate15to16 import do_migration\n        else:\n            raise Exception(f\"DB migration of version {current} to {current+1} is not available\")\n        try:\n            do_migration(conf)\n        except Exception:\n            log.exception(\"failed to migrate database\")\n            if os.path.exists(os.path.join(conf.data_dir, \"lbrynet.sqlite\")):\n                backup_name = f\"rev_{current}_unmigrated_database\"\n                count = 0\n                while os.path.exists(os.path.join(conf.data_dir, backup_name + \".sqlite\")):\n                    count += 1\n                    backup_name = f\"rev_{current}_unmigrated_database_{count}\"\n                backup_path = os.path.join(conf.data_dir, backup_name + \".sqlite\")\n                os.rename(os.path.join(conf.data_dir, \"lbrynet.sqlite\"), backup_path)\n                log.info(\"made a backup of the unmigrated database: %s\", backup_path)\n            if os.path.isfile(os.path.join(conf.data_dir, \"db_revision\")):\n                os.remove(os.path.join(conf.data_dir, \"db_revision\"))\n            return None\n        current += 1\n        log.info(\"successfully migrated the database from revision %i to %i\", current - 1, current)\n    return None\n\n\ndef run_migration_script():\n    log_format = \"(%(asctime)s)[%(filename)s:%(lineno)s] %(funcName)s(): %(message)s\"\n    logging.basicConfig(level=logging.DEBUG, format=log_format, filename=\"migrator.log\")\n    sys.stdout = open(\"migrator.out.log\", 'w')\n    sys.stderr = open(\"migrator.err.log\", 'w')\n    migrate_db(sys.argv[1], int(sys.argv[2]), int(sys.argv[3]))\n\n\nif __name__ == \"__main__\":\n    run_migration_script()\n"
  },
  {
    "path": "lbry/extras/daemon/migrator/migrate10to11.py",
    "content": "import sqlite3\nimport os\nimport binascii\n\n\ndef do_migration(conf):\n    db_path = os.path.join(conf.data_dir, \"lbrynet.sqlite\")\n    connection = sqlite3.connect(db_path)\n    cursor = connection.cursor()\n\n    current_columns = []\n    for col_info in cursor.execute(\"pragma table_info('file');\").fetchall():\n        current_columns.append(col_info[1])\n    if 'content_fee' in current_columns or 'saved_file' in current_columns:\n        connection.close()\n        print(\"already migrated\")\n        return\n\n    cursor.execute(\n        \"pragma foreign_keys=off;\"\n    )\n\n    cursor.execute(\"\"\"\n        create table if not exists new_file (\n            stream_hash text primary key not null references stream,\n            file_name text,\n            download_directory text,\n            blob_data_rate real not null,\n            status text not null,\n            saved_file integer not null,\n            content_fee text\n        );\n    \"\"\")\n    for (stream_hash, file_name, download_dir, data_rate, status) in cursor.execute(\"select * from file\").fetchall():\n        saved_file = 0\n        if download_dir != '{stream}' and file_name != '{stream}':\n            try:\n                if os.path.isfile(os.path.join(binascii.unhexlify(download_dir).decode(),\n                                  binascii.unhexlify(file_name).decode())):\n                    saved_file = 1\n                else:\n                    download_dir, file_name = None, None\n            except Exception:\n                download_dir, file_name = None, None\n        else:\n            download_dir, file_name = None, None\n        cursor.execute(\n            \"insert into new_file values (?, ?, ?, ?, ?, ?, NULL)\",\n            (stream_hash, file_name, download_dir, data_rate, status, saved_file)\n        )\n    cursor.execute(\"drop table file\")\n    cursor.execute(\"alter table new_file rename to file\")\n    connection.commit()\n    connection.close()\n"
  },
  {
    "path": "lbry/extras/daemon/migrator/migrate11to12.py",
    "content": "import sqlite3\nimport os\nimport time\n\n\ndef do_migration(conf):\n    db_path = os.path.join(conf.data_dir, 'lbrynet.sqlite')\n    connection = sqlite3.connect(db_path)\n    connection.row_factory = sqlite3.Row\n    cursor = connection.cursor()\n\n    current_columns = []\n    for col_info in cursor.execute(\"pragma table_info('file');\").fetchall():\n        current_columns.append(col_info[1])\n\n    if 'added_on' in current_columns:\n        connection.close()\n        print('already migrated')\n        return\n\n    # follow 12 step schema change procedure\n    cursor.execute(\"pragma foreign_keys=off\")\n\n    # we don't have any indexes, views or triggers, so step 3 is skipped.\n    cursor.execute(\"drop table if exists new_file\")\n    cursor.execute(\"\"\"\n        create table if not exists new_file (\n            stream_hash         text    not null    primary key     references stream,\n            file_name           text,\n            download_directory  text,\n            blob_data_rate      text    not null,\n            status              text    not null,\n            saved_file          integer not null,\n            content_fee         text,\n            added_on            integer not null\n        );\n\n\n    \"\"\")\n\n    # step 5: transfer content from old to new\n    select = \"select * from file\"\n    for (stream_hash, file_name, download_dir, blob_rate, status, saved_file, fee) \\\n            in cursor.execute(select).fetchall():\n        added_on = int(time.time())\n        cursor.execute(\n            \"insert into new_file values (?, ?, ?, ?, ?, ?, ?, ?)\",\n            (stream_hash, file_name, download_dir, blob_rate, status, saved_file, fee, added_on)\n        )\n\n    # step 6: drop old table\n    cursor.execute(\"drop table file\")\n\n    # step 7: rename new table to old table\n    cursor.execute(\"alter table new_file rename to file\")\n\n    # step 8: we aren't using indexes, views or triggers so skip\n    # step 9: no views so skip\n    # step 10: foreign key check\n    cursor.execute(\"pragma foreign_key_check;\")\n\n    # step 11: commit transaction\n    connection.commit()\n\n    # step 12: re-enable foreign keys\n    connection.execute(\"pragma foreign_keys=on;\")\n\n    # done :)\n    connection.close()\n"
  },
  {
    "path": "lbry/extras/daemon/migrator/migrate12to13.py",
    "content": "import os\nimport sqlite3\n\n\ndef do_migration(conf):\n    db_path = os.path.join(conf.data_dir, \"lbrynet.sqlite\")\n    connection = sqlite3.connect(db_path)\n    cursor = connection.cursor()\n\n    current_columns = []\n    for col_info in cursor.execute(\"pragma table_info('file');\").fetchall():\n        current_columns.append(col_info[1])\n    if 'bt_infohash' in current_columns:\n        connection.close()\n        print(\"already migrated\")\n        return\n\n    cursor.executescript(\"\"\"\n        pragma foreign_keys=off;\n\n        create table if not exists torrent (\n            bt_infohash char(20) not null primary key,\n            tracker text,\n            length integer not null,\n            name text not null\n        );\n\n        create table if not exists torrent_node ( -- BEP-0005\n            bt_infohash char(20) not null references torrent,\n            host text not null,\n            port integer not null\n        );\n\n        create table if not exists torrent_tracker ( -- BEP-0012\n            bt_infohash char(20) not null references torrent,\n            tracker text not null\n        );\n\n        create table if not exists torrent_http_seed ( -- BEP-0017\n            bt_infohash char(20) not null references torrent,\n            http_seed text not null\n        );\n\n        create table if not exists new_file (\n            stream_hash char(96) references stream,\n            bt_infohash char(20) references torrent,\n            file_name text,\n            download_directory text,\n            blob_data_rate real not null,\n            status text not null,\n            saved_file integer not null,\n            content_fee text,\n            added_on integer not null\n        );\n\n        create table if not exists new_content_claim (\n            stream_hash char(96) references stream,\n            bt_infohash char(20) references torrent,\n            claim_outpoint text unique not null references claim\n        );\n\n        insert into new_file (stream_hash, bt_infohash, file_name, download_directory, blob_data_rate, status,\n            saved_file, content_fee, added_on) select\n                stream_hash, NULL, file_name, download_directory, blob_data_rate, status, saved_file, content_fee,\n                added_on\n            from file;\n\n        insert or ignore into new_content_claim (stream_hash, bt_infohash, claim_outpoint)\n            select stream_hash, NULL, claim_outpoint from content_claim;\n\n        drop table file;\n        drop table content_claim;\n        alter table new_file rename to file;\n        alter table new_content_claim rename to content_claim;\n\n        pragma foreign_keys=on;\n    \"\"\")\n\n    connection.commit()\n    connection.close()\n"
  },
  {
    "path": "lbry/extras/daemon/migrator/migrate13to14.py",
    "content": "import os\nimport sqlite3\n\n\ndef do_migration(conf):\n    db_path = os.path.join(conf.data_dir, \"lbrynet.sqlite\")\n    connection = sqlite3.connect(db_path)\n    cursor = connection.cursor()\n\n    cursor.executescript(\"\"\"\n        create table if not exists peer (\n            node_id char(96) not null primary key,\n            address text not null,\n            udp_port integer not null,\n            tcp_port integer,\n            unique (address, udp_port)\n        );\n    \"\"\")\n\n    connection.commit()\n    connection.close()\n"
  },
  {
    "path": "lbry/extras/daemon/migrator/migrate14to15.py",
    "content": "import os\nimport sqlite3\n\n\ndef do_migration(conf):\n    db_path = os.path.join(conf.data_dir, \"lbrynet.sqlite\")\n    connection = sqlite3.connect(db_path)\n    cursor = connection.cursor()\n\n    cursor.executescript(\"\"\"\n        alter table blob add column added_on integer not null default 0;\n        alter table blob add column is_mine integer not null default 1;\n    \"\"\")\n\n    connection.commit()\n    connection.close()\n"
  },
  {
    "path": "lbry/extras/daemon/migrator/migrate15to16.py",
    "content": "import os\nimport sqlite3\n\n\ndef do_migration(conf):\n    db_path = os.path.join(conf.data_dir, \"lbrynet.sqlite\")\n    connection = sqlite3.connect(db_path)\n    cursor = connection.cursor()\n\n    cursor.executescript(\"\"\"\n        update blob set should_announce=0\n        where should_announce=1 and \n        blob.blob_hash in (select stream_blob.blob_hash from stream_blob where position=0);\n    \"\"\")\n\n    connection.commit()\n    connection.close()\n"
  },
  {
    "path": "lbry/extras/daemon/migrator/migrate1to2.py",
    "content": "import sqlite3\nimport os\nimport logging\n\nlog = logging.getLogger(__name__)\nUNSET_NOUT = -1\n\ndef do_migration(conf):\n    log.info(\"Doing the migration\")\n    migrate_blockchainname_db(conf.data_dir)\n    log.info(\"Migration succeeded\")\n\n\ndef migrate_blockchainname_db(db_dir):\n    blockchainname_db = os.path.join(db_dir, \"blockchainname.db\")\n    # skip migration on fresh installs\n    if not os.path.isfile(blockchainname_db):\n        return\n    temp_db = sqlite3.connect(\":memory:\")\n    db_file = sqlite3.connect(blockchainname_db)\n    file_cursor = db_file.cursor()\n    mem_cursor = temp_db.cursor()\n\n    mem_cursor.execute(\"create table if not exists name_metadata (\"\n                       \"    name text, \"\n                       \"    txid text, \"\n                       \"    n integer, \"\n                       \"    sd_hash text)\")\n    mem_cursor.execute(\"create table if not exists claim_ids (\"\n                       \"    claimId text, \"\n                       \"    name text, \"\n                       \"    txid text, \"\n                       \"    n integer)\")\n    temp_db.commit()\n\n    name_metadata = file_cursor.execute(\"select * from name_metadata\").fetchall()\n    claim_metadata = file_cursor.execute(\"select * from claim_ids\").fetchall()\n\n    # fill n as V1_UNSET_NOUT, Wallet.py will be responsible for filling in correct n\n    for name, txid, sd_hash in name_metadata:\n        mem_cursor.execute(\n            \"insert into name_metadata values (?, ?, ?, ?) \",\n            (name, txid, UNSET_NOUT, sd_hash))\n\n    for claim_id, name, txid in claim_metadata:\n        mem_cursor.execute(\n            \"insert into claim_ids values (?, ?, ?, ?)\",\n            (claim_id, name, txid, UNSET_NOUT))\n    temp_db.commit()\n\n    new_name_metadata = mem_cursor.execute(\"select * from name_metadata\").fetchall()\n    new_claim_metadata = mem_cursor.execute(\"select * from claim_ids\").fetchall()\n\n    file_cursor.execute(\"drop table name_metadata\")\n    file_cursor.execute(\"create table name_metadata (\"\n                        \"    name text, \"\n                        \"    txid text, \"\n                        \"    n integer, \"\n                        \"    sd_hash text)\")\n\n    for name, txid, n, sd_hash in new_name_metadata:\n        file_cursor.execute(\n            \"insert into name_metadata values (?, ?, ?, ?) \", (name, txid, n, sd_hash))\n\n    file_cursor.execute(\"drop table claim_ids\")\n    file_cursor.execute(\"create table claim_ids (\"\n                        \"    claimId text, \"\n                        \"    name text, \"\n                        \"    txid text, \"\n                        \"    n integer)\")\n\n    for claim_id, name, txid, n in new_claim_metadata:\n        file_cursor.execute(\"insert into claim_ids values (?, ?, ?, ?)\", (claim_id, name, txid, n))\n\n    db_file.commit()\n    db_file.close()\n    temp_db.close()\n"
  },
  {
    "path": "lbry/extras/daemon/migrator/migrate2to3.py",
    "content": "import sqlite3\nimport os\nimport logging\n\nlog = logging.getLogger(__name__)\n\n\ndef do_migration(conf):\n    log.info(\"Doing the migration\")\n    migrate_blockchainname_db(conf.data_dir)\n    log.info(\"Migration succeeded\")\n\n\ndef migrate_blockchainname_db(db_dir):\n    blockchainname_db = os.path.join(db_dir, \"blockchainname.db\")\n    # skip migration on fresh installs\n    if not os.path.isfile(blockchainname_db):\n        return\n\n    db_file = sqlite3.connect(blockchainname_db)\n    file_cursor = db_file.cursor()\n\n    tables = file_cursor.execute(\"SELECT tbl_name FROM sqlite_master \"\n                                 \"WHERE type='table'\").fetchall()\n\n    if 'tmp_name_metadata_table' in tables and 'name_metadata' not in tables:\n        file_cursor.execute(\"ALTER TABLE tmp_name_metadata_table RENAME TO name_metadata\")\n    else:\n        file_cursor.executescript(\n            \"CREATE TABLE IF NOT EXISTS tmp_name_metadata_table \"\n            \"    (name TEXT UNIQUE NOT NULL, \"\n            \"     txid TEXT NOT NULL, \"\n            \"     n INTEGER NOT NULL, \"\n            \"     sd_hash TEXT NOT NULL); \"\n            \"INSERT OR IGNORE INTO tmp_name_metadata_table \"\n            \"    (name, txid, n, sd_hash) \"\n            \"    SELECT name, txid, n, sd_hash FROM name_metadata; \"\n            \"DROP TABLE name_metadata; \"\n            \"ALTER TABLE tmp_name_metadata_table RENAME TO name_metadata;\"\n        )\n    db_file.commit()\n    db_file.close()\n"
  },
  {
    "path": "lbry/extras/daemon/migrator/migrate3to4.py",
    "content": "import sqlite3\nimport os\nimport logging\n\nlog = logging.getLogger(__name__)\n\n\ndef do_migration(conf):\n    log.info(\"Doing the migration\")\n    migrate_blobs_db(conf.data_dir)\n    log.info(\"Migration succeeded\")\n\n\ndef migrate_blobs_db(db_dir):\n    \"\"\"\n    We migrate the blobs.db used in BlobManager to have a \"should_announce\" column,\n    and set this to True for blobs that are sd_hash's or head blobs (first blob in stream)\n    \"\"\"\n\n    blobs_db = os.path.join(db_dir, \"blobs.db\")\n    lbryfile_info_db = os.path.join(db_dir, 'lbryfile_info.db')\n\n    # skip migration on fresh installs\n    if not os.path.isfile(blobs_db) and not os.path.isfile(lbryfile_info_db):\n        return\n\n    # if blobs.db doesn't exist, skip migration\n    if not os.path.isfile(blobs_db):\n        log.info(\"blobs.db was not found but lbryfile_info.db was found, skipping migration\")\n        return\n\n    blobs_db_file = sqlite3.connect(blobs_db)\n    blobs_db_cursor = blobs_db_file.cursor()\n\n    # check if new columns exist (it shouldn't) and create it\n    try:\n        blobs_db_cursor.execute(\"SELECT should_announce FROM blobs\")\n    except sqlite3.OperationalError:\n        blobs_db_cursor.execute(\n            \"ALTER TABLE blobs ADD COLUMN should_announce integer NOT NULL DEFAULT 0\")\n    else:\n        log.warning(\"should_announce already exists somehow, proceeding anyways\")\n\n    # if lbryfile_info.db doesn't exist, skip marking blobs as should_announce = True\n    if not os.path.isfile(lbryfile_info_db):\n        log.error(\"lbryfile_info.db was not found, skipping check for should_announce\")\n        return\n\n    lbryfile_info_file = sqlite3.connect(lbryfile_info_db)\n    lbryfile_info_cursor = lbryfile_info_file.cursor()\n\n    # find blobs that are stream descriptors\n    lbryfile_info_cursor.execute('SELECT * FROM lbry_file_descriptors')\n    descriptors = lbryfile_info_cursor.fetchall()\n    should_announce_blob_hashes = []\n    for d in descriptors:\n        sd_blob_hash = (d[0],)\n        should_announce_blob_hashes.append(sd_blob_hash)\n\n    # find blobs that are the first blob in a stream\n    lbryfile_info_cursor.execute('SELECT * FROM lbry_file_blobs WHERE position = 0')\n    blobs = lbryfile_info_cursor.fetchall()\n    head_blob_hashes = []\n    for b in blobs:\n        blob_hash = (b[0],)\n        should_announce_blob_hashes.append(blob_hash)\n\n    # now mark them as should_announce = True\n    blobs_db_cursor.executemany('UPDATE blobs SET should_announce=1 WHERE blob_hash=?',\n                                should_announce_blob_hashes)\n\n    # Now run some final checks here to make sure migration succeeded\n    try:\n        blobs_db_cursor.execute(\"SELECT should_announce FROM blobs\")\n    except sqlite3.OperationalError:\n        raise Exception('Migration failed, cannot find should_announce')\n\n    blobs_db_cursor.execute(\"SELECT * FROM blobs WHERE should_announce=1\")\n    blobs = blobs_db_cursor.fetchall()\n    if len(blobs) != len(should_announce_blob_hashes):\n        log.error(\"Some how not all blobs were marked as announceable\")\n\n    blobs_db_file.commit()\n    blobs_db_file.close()\n    lbryfile_info_file.close()\n"
  },
  {
    "path": "lbry/extras/daemon/migrator/migrate4to5.py",
    "content": "import sqlite3\nimport os\nimport logging\n\nlog = logging.getLogger(__name__)\n\n\ndef do_migration(conf):\n    log.info(\"Doing the migration\")\n    add_lbry_file_metadata(conf.data_dir)\n    log.info(\"Migration succeeded\")\n\n\ndef add_lbry_file_metadata(db_dir):\n    \"\"\"\n    We migrate the blobs.db used in BlobManager to have a \"should_announce\" column,\n    and set this to True for blobs that are sd_hash's or head blobs (first blob in stream)\n    \"\"\"\n\n    name_metadata = os.path.join(db_dir, \"blockchainname.db\")\n    lbryfile_info_db = os.path.join(db_dir, 'lbryfile_info.db')\n\n    if not os.path.isfile(name_metadata) and not os.path.isfile(lbryfile_info_db):\n        return\n\n    if not os.path.isfile(lbryfile_info_db):\n        log.info(\"blockchainname.db was not found but lbryfile_info.db was found, skipping migration\")\n        return\n\n    name_metadata_db = sqlite3.connect(name_metadata)\n    lbryfile_db = sqlite3.connect(lbryfile_info_db)\n    name_metadata_cursor = name_metadata_db.cursor()\n    lbryfile_cursor = lbryfile_db.cursor()\n\n    lbryfile_db.executescript(\n        \"create table if not exists lbry_file_metadata (\" +\n        \"    lbry_file integer primary key, \" +\n        \"    txid text, \" +\n        \"    n integer, \" +\n        \"    foreign key(lbry_file) references lbry_files(rowid)\"\n        \")\")\n\n    _files = lbryfile_cursor.execute(\"select rowid, stream_hash from lbry_files\").fetchall()\n\n    lbry_files = {x[1]: x[0] for x in _files}\n    for (sd_hash, stream_hash) in lbryfile_cursor.execute(\"select * \"\n                                                          \"from lbry_file_descriptors\").fetchall():\n        lbry_file_id = lbry_files[stream_hash]\n        outpoint = name_metadata_cursor.execute(\"select txid, n from name_metadata \"\n                                                \"where sd_hash=?\",\n                                                (sd_hash,)).fetchall()\n        if outpoint:\n            txid, nout = outpoint[0]\n            lbryfile_cursor.execute(\"insert into lbry_file_metadata values (?, ?, ?)\",\n                                    (lbry_file_id, txid, nout))\n        else:\n            lbryfile_cursor.execute(\"insert into lbry_file_metadata values (?, ?, ?)\",\n                                    (lbry_file_id, None, None))\n    lbryfile_db.commit()\n\n    lbryfile_db.close()\n    name_metadata_db.close()\n"
  },
  {
    "path": "lbry/extras/daemon/migrator/migrate5to6.py",
    "content": "import sqlite3\nimport os\nimport json\nimport logging\nfrom binascii import hexlify\nfrom lbry.schema.claim import Claim\n\nlog = logging.getLogger(__name__)\n\nCREATE_TABLES_QUERY = \"\"\"\n            pragma foreign_keys=on;\n            pragma journal_mode=WAL;\n\n            create table if not exists blob (\n                blob_hash char(96) primary key not null,\n                blob_length integer not null,\n                next_announce_time integer not null,\n                should_announce integer not null default 0,\n                status text not null\n            );\n\n            create table if not exists stream (\n                stream_hash char(96) not null primary key,\n                sd_hash char(96) not null references blob,\n                stream_key text not null,\n                stream_name text not null,\n                suggested_filename text not null\n            );\n\n            create table if not exists stream_blob (\n                stream_hash char(96) not null references stream,\n                blob_hash char(96) references blob,\n                position integer not null,\n                iv char(32) not null,\n                primary key (stream_hash, blob_hash)\n            );\n\n            create table if not exists claim (\n                claim_outpoint text not null primary key,\n                claim_id char(40) not null,\n                claim_name text not null,\n                amount integer not null,\n                height integer not null,\n                serialized_metadata blob not null,\n                channel_claim_id text,\n                address text not null,\n                claim_sequence integer not null\n            );\n\n            create table if not exists file (\n                stream_hash text primary key not null references stream,\n                file_name text not null,\n                download_directory text not null,\n                blob_data_rate real not null,\n                status text not null\n            );\n\n            create table if not exists content_claim (\n                stream_hash text unique not null references file,\n                claim_outpoint text not null references claim,\n                primary key (stream_hash, claim_outpoint)\n            );\n\n            create table if not exists support (\n                support_outpoint text not null primary key,\n                claim_id text not null,\n                amount integer not null,\n                address text not null\n            );\n    \"\"\"\n\n\ndef run_operation(db):\n    def _decorate(fn):\n        def _wrapper(*args):\n            cursor = db.cursor()\n            try:\n                result = fn(cursor, *args)\n                db.commit()\n                return result\n            except sqlite3.IntegrityError:\n                db.rollback()\n                raise\n        return _wrapper\n    return _decorate\n\n\ndef verify_sd_blob(sd_hash, blob_dir):\n    with open(os.path.join(blob_dir, sd_hash), \"r\") as sd_file:\n        data = sd_file.read()\n        sd_length = len(data)\n        decoded = json.loads(data)\n    assert set(decoded.keys()) == {\n        'stream_name', 'blobs', 'stream_type', 'key', 'suggested_file_name', 'stream_hash'\n    }, \"invalid sd blob\"\n    for blob in sorted(decoded['blobs'], key=lambda x: int(x['blob_num']), reverse=True):\n        if blob['blob_num'] == len(decoded['blobs']) - 1:\n            assert {'length', 'blob_num', 'iv'} == set(blob.keys()), 'invalid stream terminator'\n            assert blob['length'] == 0, 'non zero length stream terminator'\n        else:\n            assert {'blob_hash', 'length', 'blob_num', 'iv'} == set(blob.keys()), 'invalid stream blob'\n            assert blob['length'] > 0, 'zero length stream blob'\n    return decoded, sd_length\n\n\ndef do_migration(conf):\n    new_db_path = os.path.join(conf.data_dir, \"lbrynet.sqlite\")\n    connection = sqlite3.connect(new_db_path)\n\n    metadata_db = sqlite3.connect(os.path.join(conf.data_dir, \"blockchainname.db\"))\n    lbryfile_db = sqlite3.connect(os.path.join(conf.data_dir, 'lbryfile_info.db'))\n    blobs_db = sqlite3.connect(os.path.join(conf.data_dir, 'blobs.db'))\n\n    name_metadata_cursor = metadata_db.cursor()\n    lbryfile_cursor = lbryfile_db.cursor()\n    blobs_db_cursor = blobs_db.cursor()\n\n    old_rowid_to_outpoint = {\n        rowid: (txid, nout) for (rowid, txid, nout) in\n        lbryfile_cursor.execute(\"select * from lbry_file_metadata\").fetchall()\n    }\n\n    old_sd_hash_to_outpoint = {\n        sd_hash: (txid, nout) for (txid, nout, sd_hash) in\n        name_metadata_cursor.execute(\"select txid, n, sd_hash from name_metadata\").fetchall()\n    }\n\n    sd_hash_to_stream_hash = dict(\n        lbryfile_cursor.execute(\"select sd_blob_hash, stream_hash from lbry_file_descriptors\").fetchall()\n    )\n\n    stream_hash_to_stream_blobs = {}\n\n    for (blob_hash, stream_hash, position, iv, length) in lbryfile_db.execute(\n            \"select * from lbry_file_blobs\").fetchall():\n        stream_blobs = stream_hash_to_stream_blobs.get(stream_hash, [])\n        stream_blobs.append((blob_hash, length, position, iv))\n        stream_hash_to_stream_blobs[stream_hash] = stream_blobs\n\n    claim_outpoint_queries = {}\n\n    for claim_query in metadata_db.execute(\n            \"select distinct c.txid, c.n, c.claimId, c.name, claim_cache.claim_sequence, claim_cache.claim_address, \"\n            \"claim_cache.height, claim_cache.amount, claim_cache.claim_pb \"\n            \"from claim_cache inner join claim_ids c on claim_cache.claim_id=c.claimId\"):\n        txid, nout = claim_query[0], claim_query[1]\n        if (txid, nout) in claim_outpoint_queries:\n            continue\n        claim_outpoint_queries[(txid, nout)] = claim_query\n\n    @run_operation(connection)\n    def _populate_blobs(transaction, blob_infos):\n        transaction.executemany(\n            \"insert into blob values (?, ?, ?, ?, ?)\",\n            [(blob_hash, blob_length, int(next_announce_time), should_announce, \"finished\")\n             for (blob_hash, blob_length, _, next_announce_time, should_announce) in blob_infos]\n        )\n\n    @run_operation(connection)\n    def _import_file(transaction, sd_hash, stream_hash, key, stream_name, suggested_file_name, data_rate,\n                     status, stream_blobs):\n        try:\n            transaction.execute(\n                \"insert or ignore into stream values (?, ?, ?, ?, ?)\",\n                (stream_hash, sd_hash, key, stream_name, suggested_file_name)\n            )\n        except sqlite3.IntegrityError:\n            # failed because the sd isn't a known blob, we'll try to read the blob file and recover it\n            return sd_hash\n\n        # insert any stream blobs that were missing from the blobs table\n        transaction.executemany(\n            \"insert or ignore into blob values (?, ?, ?, ?, ?)\",\n            [\n                (blob_hash, length, 0, 0, \"pending\")\n                for (blob_hash, length, position, iv) in stream_blobs\n            ]\n        )\n\n        # insert the stream blobs\n        for blob_hash, length, position, iv in stream_blobs:\n            transaction.execute(\n                \"insert or ignore into stream_blob values (?, ?, ?, ?)\",\n                (stream_hash, blob_hash, position, iv)\n            )\n\n        download_dir = conf.download_dir\n        if not isinstance(download_dir, bytes):\n            download_dir = download_dir.encode()\n\n        # insert the file\n        transaction.execute(\n            \"insert or ignore into file values (?, ?, ?, ?, ?)\",\n            (stream_hash, stream_name, hexlify(download_dir),\n             data_rate, status)\n        )\n\n    @run_operation(connection)\n    def _add_recovered_blobs(transaction, blob_infos, sd_hash, sd_length):\n        transaction.execute(\n            \"insert or replace into blob values (?, ?, ?, ?, ?)\", (sd_hash, sd_length, 0, 1, \"finished\")\n        )\n        for blob in sorted(blob_infos, key=lambda x: x['blob_num'], reverse=True):\n            if blob['blob_num'] < len(blob_infos) - 1:\n                transaction.execute(\n                    \"insert or ignore into blob values (?, ?, ?, ?, ?)\",\n                    (blob['blob_hash'], blob['length'], 0, 0, \"pending\")\n                )\n\n    @run_operation(connection)\n    def _make_db(new_db):\n        # create the new tables\n        new_db.executescript(CREATE_TABLES_QUERY)\n\n        # first migrate the blobs\n        blobs = blobs_db_cursor.execute(\"select * from blobs\").fetchall()\n        _populate_blobs(blobs)  # pylint: disable=no-value-for-parameter\n        log.info(\"migrated %i blobs\", new_db.execute(\"select count(*) from blob\").fetchone()[0])\n\n        # used to store the query arguments if we need to try re-importing the lbry file later\n        file_args = {}  # <sd_hash>: args tuple\n\n        file_outpoints = {}  # <outpoint tuple>: sd_hash\n\n        # get the file and stream queries ready\n        for (rowid, sd_hash, stream_hash, key, stream_name, suggested_file_name, data_rate, status) in \\\n            lbryfile_db.execute(\n                \"select distinct lbry_files.rowid, d.sd_blob_hash, lbry_files.*, o.blob_data_rate, o.status \"\n                \"from lbry_files \"\n                \"inner join lbry_file_descriptors d on lbry_files.stream_hash=d.stream_hash \"\n                \"inner join lbry_file_options o on lbry_files.stream_hash=o.stream_hash\"):\n\n            # this is try to link the file to a content claim after we've imported all the files\n            if rowid in old_rowid_to_outpoint:\n                file_outpoints[old_rowid_to_outpoint[rowid]] = sd_hash\n            elif sd_hash in old_sd_hash_to_outpoint:\n                file_outpoints[old_sd_hash_to_outpoint[sd_hash]] = sd_hash\n\n            sd_hash_to_stream_hash[sd_hash] = stream_hash\n            if stream_hash in stream_hash_to_stream_blobs:\n                file_args[sd_hash] = (\n                    sd_hash, stream_hash, key, stream_name,\n                    suggested_file_name, data_rate or 0.0,\n                    status, stream_hash_to_stream_blobs.pop(stream_hash)\n                )\n\n        # used to store the query arguments if we need to try re-importing the claim\n        claim_queries = {}  # <sd_hash>: claim query tuple\n\n        # get the claim queries ready, only keep those with associated files\n        for outpoint, sd_hash in file_outpoints.items():\n            if outpoint in claim_outpoint_queries:\n                claim_queries[sd_hash] = claim_outpoint_queries[outpoint]\n\n        # insert the claims\n        new_db.executemany(\n            \"insert or ignore into claim values (?, ?, ?, ?, ?, ?, ?, ?, ?)\",\n            [\n                (\n                    \"%s:%i\" % (claim_arg_tup[0], claim_arg_tup[1]), claim_arg_tup[2], claim_arg_tup[3],\n                    claim_arg_tup[7], claim_arg_tup[6], claim_arg_tup[8],\n                    Claim.from_bytes(claim_arg_tup[8]).signing_channel_id, claim_arg_tup[5], claim_arg_tup[4]\n                )\n                for sd_hash, claim_arg_tup in claim_queries.items() if claim_arg_tup\n            ]     # sd_hash,  (txid, nout, claim_id, name, sequence, address, height, amount, serialized)\n        )\n\n        log.info(\"migrated %i claims\", new_db.execute(\"select count(*) from claim\").fetchone()[0])\n\n        damaged_stream_sds = []\n        # import the files and get sd hashes of streams to attempt recovering\n        for sd_hash, file_query in file_args.items():\n            failed_sd = _import_file(*file_query)\n            if failed_sd:\n                damaged_stream_sds.append(failed_sd)\n\n        # recover damaged streams\n        if damaged_stream_sds:\n            blob_dir = os.path.join(conf.data_dir, \"blobfiles\")\n            damaged_sds_on_disk = [] if not os.path.isdir(blob_dir) else list({p for p in os.listdir(blob_dir)\n                                                                               if p in damaged_stream_sds})\n            for damaged_sd in damaged_sds_on_disk:\n                try:\n                    decoded, sd_length = verify_sd_blob(damaged_sd, blob_dir)\n                    blobs = decoded['blobs']\n                    _add_recovered_blobs(blobs, damaged_sd, sd_length)  # pylint: disable=no-value-for-parameter\n                    _import_file(*file_args[damaged_sd])\n                    damaged_stream_sds.remove(damaged_sd)\n                except (OSError, ValueError, TypeError, AssertionError, sqlite3.IntegrityError):\n                    continue\n\n        log.info(\"migrated %i files\", new_db.execute(\"select count(*) from file\").fetchone()[0])\n\n        # associate the content claims to their respective files\n        for claim_arg_tup in claim_queries.values():\n            if claim_arg_tup and (claim_arg_tup[0], claim_arg_tup[1]) in file_outpoints \\\n                    and file_outpoints[(claim_arg_tup[0], claim_arg_tup[1])] in sd_hash_to_stream_hash:\n                try:\n                    new_db.execute(\n                        \"insert or ignore into content_claim values (?, ?)\",\n                        (\n                            sd_hash_to_stream_hash.get(file_outpoints.get((claim_arg_tup[0], claim_arg_tup[1]))),\n                            \"%s:%i\" % (claim_arg_tup[0], claim_arg_tup[1])\n                        )\n                    )\n                except sqlite3.IntegrityError:\n                    continue\n\n        log.info(\"migrated %i content claims\", new_db.execute(\"select count(*) from content_claim\").fetchone()[0])\n    try:\n        _make_db()  # pylint: disable=no-value-for-parameter\n    except sqlite3.OperationalError as err:\n        if err.message == \"table blob has 7 columns but 5 values were supplied\":\n            log.warning(\"detected a failed previous migration to revision 6, repairing it\")\n            connection.close()\n            os.remove(new_db_path)\n            return do_migration(conf)\n        raise err\n\n    connection.close()\n    blobs_db.close()\n    lbryfile_db.close()\n    metadata_db.close()\n    # os.remove(os.path.join(db_dir, \"blockchainname.db\"))\n    # os.remove(os.path.join(db_dir, 'lbryfile_info.db'))\n    # os.remove(os.path.join(db_dir, 'blobs.db'))\n"
  },
  {
    "path": "lbry/extras/daemon/migrator/migrate6to7.py",
    "content": "import sqlite3\nimport os\n\n\ndef do_migration(conf):\n    db_path = os.path.join(conf.data_dir, \"lbrynet.sqlite\")\n    connection = sqlite3.connect(db_path)\n    cursor = connection.cursor()\n    cursor.executescript(\"alter table blob add last_announced_time integer;\")\n    cursor.executescript(\"alter table blob add single_announce integer;\")\n    cursor.execute(\"update blob set next_announce_time=0\")\n    connection.commit()\n    connection.close()\n"
  },
  {
    "path": "lbry/extras/daemon/migrator/migrate7to8.py",
    "content": "import sqlite3\nimport os\n\n\ndef do_migration(conf):\n    db_path = os.path.join(conf.data_dir, \"lbrynet.sqlite\")\n    connection = sqlite3.connect(db_path)\n    cursor = connection.cursor()\n\n    cursor.executescript(\n        \"\"\"\n        create table reflected_stream (\n            sd_hash text not null,\n            reflector_address text not null,\n            timestamp integer,\n            primary key (sd_hash, reflector_address)\n        );\n        \"\"\"\n    )\n    connection.commit()\n    connection.close()\n"
  },
  {
    "path": "lbry/extras/daemon/migrator/migrate8to9.py",
    "content": "import sqlite3\nimport logging\nimport os\nfrom lbry.blob.blob_info import BlobInfo\nfrom lbry.stream.descriptor import StreamDescriptor\n\nlog = logging.getLogger(__name__)\n\n\ndef do_migration(conf):\n    db_path = os.path.join(conf.data_dir, \"lbrynet.sqlite\")\n    blob_dir = os.path.join(conf.data_dir, \"blobfiles\")\n    connection = sqlite3.connect(db_path)\n    cursor = connection.cursor()\n\n    query = \"select stream_name, stream_key, suggested_filename, sd_hash, stream_hash from stream\"\n    streams = cursor.execute(query).fetchall()\n\n    blobs = cursor.execute(\"select s.stream_hash, s.position, s.iv, b.blob_hash, b.blob_length from stream_blob s \"\n                           \"left outer join blob b ON b.blob_hash=s.blob_hash order by s.position\").fetchall()\n    blobs_by_stream = {}\n    for stream_hash, position, iv, blob_hash, blob_length in blobs:\n        blobs_by_stream.setdefault(stream_hash, []).append(BlobInfo(position, blob_length or 0, iv, 0, blob_hash))\n\n    for stream_name, stream_key, suggested_filename, sd_hash, stream_hash in streams:\n        sd = StreamDescriptor(None, blob_dir, stream_name, stream_key, suggested_filename,\n                              blobs_by_stream[stream_hash], stream_hash, sd_hash)\n        if sd_hash != sd.calculate_sd_hash():\n            log.info(\"Stream for descriptor %s is invalid, cleaning it up\", sd_hash)\n            blob_hashes = [blob.blob_hash for blob in blobs_by_stream[stream_hash]]\n            delete_stream(cursor, stream_hash, sd_hash, blob_hashes, blob_dir)\n\n    connection.commit()\n    connection.close()\n\n\ndef delete_stream(transaction, stream_hash, sd_hash, blob_hashes, blob_dir):\n    transaction.execute(\"delete from content_claim where stream_hash=? \", (stream_hash,))\n    transaction.execute(\"delete from file where stream_hash=? \", (stream_hash, ))\n    transaction.execute(\"delete from stream_blob where stream_hash=?\", (stream_hash, ))\n    transaction.execute(\"delete from stream where stream_hash=? \", (stream_hash, ))\n    transaction.execute(\"delete from blob where blob_hash=?\", (sd_hash, ))\n    for blob_hash in blob_hashes:\n        transaction.execute(\"delete from blob where blob_hash=?\", (blob_hash, ))\n        file_path = os.path.join(blob_dir, blob_hash)\n        if os.path.isfile(file_path):\n            os.unlink(file_path)\n"
  },
  {
    "path": "lbry/extras/daemon/migrator/migrate9to10.py",
    "content": "import sqlite3\nimport os\n\n\ndef do_migration(conf):\n    db_path = os.path.join(conf.data_dir, \"lbrynet.sqlite\")\n    connection = sqlite3.connect(db_path)\n    cursor = connection.cursor()\n\n    query = \"select stream_hash, sd_hash from main.stream\"\n    for stream_hash, sd_hash in cursor.execute(query).fetchall():\n        head_blob_hash = cursor.execute(\n            \"select blob_hash from stream_blob where position = 0 and stream_hash = ?\",\n            (stream_hash,)\n        ).fetchone()\n        if not head_blob_hash:\n            continue\n        cursor.execute(\"update blob set should_announce=1 where blob_hash in (?, ?)\", (sd_hash, head_blob_hash[0],))\n    connection.commit()\n    connection.close()\n"
  },
  {
    "path": "lbry/extras/daemon/security.py",
    "content": "import logging\nfrom aiohttp import web\n\nlog = logging.getLogger(__name__)\n\n\ndef ensure_request_allowed(request, conf):\n    if is_request_allowed(request, conf):\n        return\n    if conf.allowed_origin:\n        log.warning(\n            \"API requests with Origin '%s' are not allowed, \"\n            \"configuration 'allowed_origin' limits requests to: '%s'\",\n            request.headers.get('Origin'), conf.allowed_origin\n        )\n    else:\n        log.warning(\n            \"API requests with Origin '%s' are not allowed, \"\n            \"update configuration 'allowed_origin' to enable this origin.\",\n            request.headers.get('Origin')\n        )\n    raise web.HTTPForbidden()\n\n\ndef is_request_allowed(request, conf) -> bool:\n    origin = request.headers.get('Origin')\n    return (\n        origin is None or\n        origin == conf.allowed_origin or\n        conf.allowed_origin == '*'\n    )\n"
  },
  {
    "path": "lbry/extras/daemon/storage.py",
    "content": "import os\nimport logging\nimport sqlite3\nimport typing\nimport asyncio\nimport binascii\nimport time\nfrom typing import Optional\nfrom lbry.wallet import SQLiteMixin\nfrom lbry.conf import Config\nfrom lbry.wallet.dewies import dewies_to_lbc, lbc_to_dewies\nfrom lbry.wallet.transaction import Transaction, Output\nfrom lbry.schema.claim import Claim\nfrom lbry.dht.constants import DATA_EXPIRATION\nfrom lbry.blob.blob_info import BlobInfo\n\nif typing.TYPE_CHECKING:\n    from lbry.blob.blob_file import BlobFile\n    from lbry.stream.descriptor import StreamDescriptor\n\nlog = logging.getLogger(__name__)\n\n\ndef calculate_effective_amount(amount: str, supports: typing.Optional[typing.List[typing.Dict]] = None) -> str:\n    return dewies_to_lbc(\n        lbc_to_dewies(amount) + sum([lbc_to_dewies(support['amount']) for support in supports])\n    )\n\n\nclass StoredContentClaim:\n    def __init__(self, outpoint: Optional[str] = None, claim_id: Optional[str] = None, name: Optional[str] = None,\n                 amount: Optional[int] = None, height: Optional[int] = None, serialized: Optional[str] = None,\n                 channel_claim_id: Optional[str] = None, address: Optional[str] = None,\n                 claim_sequence: Optional[int] = None, channel_name: Optional[str] = None):\n        self.claim_id = claim_id\n        self.outpoint = outpoint\n        self.claim_name = name\n        self.amount = amount\n        self.height = height\n        self.claim: typing.Optional[Claim] = None if not serialized else Claim.from_bytes(\n            binascii.unhexlify(serialized)\n        )\n        self.claim_address = address\n        self.claim_sequence = claim_sequence\n        self.channel_claim_id = channel_claim_id\n        self.channel_name = channel_name\n\n    @property\n    def txid(self) -> typing.Optional[str]:\n        return None if not self.outpoint else self.outpoint.split(\":\")[0]\n\n    @property\n    def nout(self) -> typing.Optional[int]:\n        return None if not self.outpoint else int(self.outpoint.split(\":\")[1])\n\n    def as_dict(self) -> typing.Dict:\n        return {\n            \"name\": self.claim_name,\n            \"claim_id\": self.claim_id,\n            \"address\": self.claim_address,\n            \"claim_sequence\": self.claim_sequence,\n            \"value\": self.claim,\n            \"height\": self.height,\n            \"amount\": dewies_to_lbc(self.amount),\n            \"nout\": self.nout,\n            \"txid\": self.txid,\n            \"channel_claim_id\": self.channel_claim_id,\n            \"channel_name\": self.channel_name\n        }\n\n\ndef _get_content_claims(transaction: sqlite3.Connection, query: str,\n                        source_hashes: typing.List[str]) -> typing.Dict[str, StoredContentClaim]:\n    claims = {}\n    for claim_info in _batched_select(transaction, query, source_hashes):\n        claims[claim_info[0]] = StoredContentClaim(*claim_info[1:])\n    return claims\n\n\ndef get_claims_from_stream_hashes(transaction: sqlite3.Connection,\n                                  stream_hashes: typing.List[str]) -> typing.Dict[str, StoredContentClaim]:\n    query = (\n        \"select content_claim.stream_hash, c.*, case when c.channel_claim_id is not null then \"\n        \"   (select claim_name from claim where claim_id==c.channel_claim_id) \"\n        \"   else null end as channel_name \"\n        \" from content_claim \"\n        \" inner join claim c on c.claim_outpoint=content_claim.claim_outpoint and content_claim.stream_hash in {}\"\n        \" order by c.rowid desc\"\n    )\n    return _get_content_claims(transaction, query, stream_hashes)\n\n\ndef get_claims_from_torrent_info_hashes(transaction: sqlite3.Connection,\n                                        info_hashes: typing.List[str]) -> typing.Dict[str, StoredContentClaim]:\n    query = (\n        \"select content_claim.bt_infohash, c.*, case when c.channel_claim_id is not null then \"\n        \"   (select claim_name from claim where claim_id==c.channel_claim_id) \"\n        \"   else null end as channel_name \"\n        \" from content_claim \"\n        \" inner join claim c on c.claim_outpoint=content_claim.claim_outpoint and content_claim.bt_infohash in {}\"\n        \" order by c.rowid desc\"\n    )\n    return _get_content_claims(transaction, query, info_hashes)\n\n\ndef _batched_select(transaction, query, parameters, batch_size=900):\n    for start_index in range(0, len(parameters), batch_size):\n        current_batch = parameters[start_index:start_index+batch_size]\n        bind = \"({})\".format(','.join(['?'] * len(current_batch)))\n        yield from transaction.execute(query.format(bind), current_batch)\n\n\ndef _get_lbry_file_stream_dict(rowid, added_on, stream_hash, file_name, download_dir, data_rate, status,\n                               sd_hash, stream_key, stream_name, suggested_file_name, claim, saved_file,\n                               raw_content_fee, fully_reflected):\n    return {\n        \"rowid\": rowid,\n        \"added_on\": added_on,\n        \"stream_hash\": stream_hash,\n        \"file_name\": file_name,                      # hex\n        \"download_directory\": download_dir,          # hex\n        \"blob_data_rate\": data_rate,\n        \"status\": status,\n        \"sd_hash\": sd_hash,\n        \"key\": stream_key,\n        \"stream_name\": stream_name,                  # hex\n        \"suggested_file_name\": suggested_file_name,  # hex\n        \"claim\": claim,\n        \"saved_file\": bool(saved_file),\n        \"content_fee\": None if not raw_content_fee else Transaction(\n            binascii.unhexlify(raw_content_fee)\n        ),\n        \"fully_reflected\": fully_reflected\n    }\n\n\ndef get_all_lbry_files(transaction: sqlite3.Connection) -> typing.List[typing.Dict]:\n    files = []\n    signed_claims = {}\n    for (rowid, stream_hash, _, file_name, download_dir, data_rate, status, saved_file, raw_content_fee,\n         added_on, _, sd_hash, stream_key, stream_name, suggested_file_name, *claim_args) in transaction.execute(\n             \"select file.rowid, file.*, stream.*, c.*, \"\n             \"  case when (SELECT 1 FROM reflected_stream r WHERE r.sd_hash=stream.sd_hash) \"\n             \"      is null then 0 else 1 end as fully_reflected \"\n             \"from file inner join stream on file.stream_hash=stream.stream_hash \"\n             \"inner join content_claim cc on file.stream_hash=cc.stream_hash \"\n             \"inner join claim c on cc.claim_outpoint=c.claim_outpoint \"\n             \"order by c.rowid desc\").fetchall():\n        claim_args, fully_reflected = tuple(claim_args[:-1]), claim_args[-1]\n        claim = StoredContentClaim(*claim_args)\n        if claim.channel_claim_id:\n            if claim.channel_claim_id not in signed_claims:\n                signed_claims[claim.channel_claim_id] = []\n            signed_claims[claim.channel_claim_id].append(claim)\n        files.append(\n            _get_lbry_file_stream_dict(\n                rowid, added_on, stream_hash, file_name, download_dir, data_rate, status,\n                sd_hash, stream_key, stream_name, suggested_file_name, claim, saved_file,\n                raw_content_fee, fully_reflected\n            )\n        )\n    for claim_name, claim_id in _batched_select(\n            transaction, \"select c.claim_name, c.claim_id from claim c where c.claim_id in {}\",\n            tuple(signed_claims.keys())):\n        for claim in signed_claims[claim_id]:\n            claim.channel_name = claim_name\n    return files\n\n\ndef store_stream(transaction: sqlite3.Connection, sd_blob: 'BlobFile', descriptor: 'StreamDescriptor'):\n    # add all blobs, except the last one, which is empty\n    transaction.executemany(\n        \"insert or ignore into blob values (?, ?, ?, ?, ?, ?, ?, ?, ?)\",\n        ((blob.blob_hash, blob.length, 0, 0, \"pending\", 0, 0, blob.added_on, blob.is_mine)\n         for blob in (descriptor.blobs[:-1] if len(descriptor.blobs) > 1 else descriptor.blobs) + [sd_blob])\n    ).fetchall()\n    # associate the blobs to the stream\n    transaction.execute(\"insert or ignore into stream values (?, ?, ?, ?, ?)\",\n                        (descriptor.stream_hash, sd_blob.blob_hash, descriptor.key,\n                         binascii.hexlify(descriptor.stream_name.encode()).decode(),\n                         binascii.hexlify(descriptor.suggested_file_name.encode()).decode())).fetchall()\n    # add the stream\n    transaction.executemany(\n        \"insert or ignore into stream_blob values (?, ?, ?, ?)\",\n        ((descriptor.stream_hash, blob.blob_hash, blob.blob_num, blob.iv)\n         for blob in descriptor.blobs)\n    ).fetchall()\n    # ensure should_announce is set regardless if insert was ignored\n    transaction.execute(\n        \"update blob set should_announce=1 where blob_hash in (?)\",\n        (sd_blob.blob_hash,)\n    ).fetchall()\n\n\ndef delete_stream(transaction: sqlite3.Connection, descriptor: 'StreamDescriptor'):\n    blob_hashes = [(blob.blob_hash, ) for blob in descriptor.blobs[:-1]]\n    blob_hashes.append((descriptor.sd_hash, ))\n    transaction.execute(\"delete from content_claim where stream_hash=? \", (descriptor.stream_hash,)).fetchall()\n    transaction.execute(\"delete from file where stream_hash=? \", (descriptor.stream_hash,)).fetchall()\n    transaction.execute(\"delete from stream_blob where stream_hash=?\", (descriptor.stream_hash,)).fetchall()\n    transaction.execute(\"delete from stream where stream_hash=? \", (descriptor.stream_hash,)).fetchall()\n    transaction.executemany(\"delete from blob where blob_hash=?\", blob_hashes).fetchall()\n\n\ndef delete_torrent(transaction: sqlite3.Connection, bt_infohash: str):\n    transaction.execute(\"delete from content_claim where bt_infohash=?\", (bt_infohash, )).fetchall()\n    transaction.execute(\"delete from torrent_tracker where bt_infohash=?\", (bt_infohash,)).fetchall()\n    transaction.execute(\"delete from torrent_node where bt_infohash=?\", (bt_infohash,)).fetchall()\n    transaction.execute(\"delete from torrent_http_seed where bt_infohash=?\", (bt_infohash,)).fetchall()\n    transaction.execute(\"delete from file where bt_infohash=?\", (bt_infohash,)).fetchall()\n    transaction.execute(\"delete from torrent where bt_infohash=?\", (bt_infohash,)).fetchall()\n\n\ndef store_file(transaction: sqlite3.Connection, stream_hash: str, file_name: typing.Optional[str],\n               download_directory: typing.Optional[str], data_payment_rate: float, status: str,\n               content_fee: typing.Optional[Transaction], added_on: typing.Optional[int] = None) -> int:\n    if not file_name and not download_directory:\n        encoded_file_name, encoded_download_dir = None, None\n    else:\n        encoded_file_name = binascii.hexlify(file_name.encode()).decode()\n        encoded_download_dir = binascii.hexlify(download_directory.encode()).decode()\n    time_added = added_on or int(time.time())\n    transaction.execute(\n        \"insert or replace into file values (?, NULL, ?, ?, ?, ?, ?, ?, ?)\",\n        (stream_hash, encoded_file_name, encoded_download_dir, data_payment_rate, status,\n         1 if (file_name and download_directory and os.path.isfile(os.path.join(download_directory, file_name))) else 0,\n         None if not content_fee else binascii.hexlify(content_fee.raw).decode(), time_added)\n    ).fetchall()\n\n    return transaction.execute(\"select rowid from file where stream_hash=?\", (stream_hash, )).fetchone()[0]\n\n\nclass SQLiteStorage(SQLiteMixin):\n    CREATE_TABLES_QUERY = \"\"\"\n            pragma foreign_keys=on;\n            pragma journal_mode=WAL;\n\n            create table if not exists blob (\n                blob_hash char(96) primary key not null,\n                blob_length integer not null,\n                next_announce_time integer not null,\n                should_announce integer not null default 0,\n                status text not null,\n                last_announced_time integer,\n                single_announce integer,\n                added_on integer not null,\n                is_mine integer not null default 0\n            );\n\n            create table if not exists stream (\n                stream_hash char(96) not null primary key,\n                sd_hash char(96) not null references blob,\n                stream_key text not null,\n                stream_name text not null,\n                suggested_filename text not null\n            );\n\n            create table if not exists stream_blob (\n                stream_hash char(96) not null references stream,\n                blob_hash char(96) references blob,\n                position integer not null,\n                iv char(32) not null,\n                primary key (stream_hash, blob_hash)\n            );\n\n            create table if not exists claim (\n                claim_outpoint text not null primary key,\n                claim_id char(40) not null,\n                claim_name text not null,\n                amount integer not null,\n                height integer not null,\n                serialized_metadata blob not null,\n                channel_claim_id text,\n                address text not null,\n                claim_sequence integer not null\n            );\n\n            create table if not exists torrent (\n                bt_infohash char(20) not null primary key,\n                tracker text,\n                length integer not null,\n                name text not null\n            );\n\n            create table if not exists torrent_node ( -- BEP-0005\n                bt_infohash char(20) not null references torrent,\n                host text not null,\n                port integer not null\n            );\n\n            create table if not exists torrent_tracker ( -- BEP-0012\n                bt_infohash char(20) not null references torrent,\n                tracker text not null\n            );\n\n            create table if not exists torrent_http_seed ( -- BEP-0017\n                bt_infohash char(20) not null references torrent,\n                http_seed text not null\n            );\n\n            create table if not exists file (\n                stream_hash char(96) references stream,\n                bt_infohash char(20) references torrent,\n                file_name text,\n                download_directory text,\n                blob_data_rate real not null,\n                status text not null,\n                saved_file integer not null,\n                content_fee text,\n                added_on integer not null\n            );\n\n            create table if not exists content_claim (\n                stream_hash char(96) references stream,\n                bt_infohash char(20) references torrent,\n                claim_outpoint text unique not null references claim\n            );\n\n            create table if not exists support (\n                support_outpoint text not null primary key,\n                claim_id text not null,\n                amount integer not null,\n                address text not null\n            );\n\n            create table if not exists reflected_stream (\n                sd_hash text not null,\n                reflector_address text not null,\n                timestamp integer,\n                primary key (sd_hash, reflector_address)\n            );\n\n            create table if not exists peer (\n                node_id char(96) not null primary key,\n                address text not null,\n                udp_port integer not null,\n                tcp_port integer,\n                unique (address, udp_port)\n            );\n            create index if not exists blob_data on blob(blob_hash, blob_length, is_mine);\n    \"\"\"\n\n    def __init__(self, conf: Config, path, loop=None, time_getter: typing.Optional[typing.Callable[[], float]] = None):\n        super().__init__(path)\n        self.conf = conf\n        self.content_claim_callbacks = {}\n        self.loop = loop or asyncio.get_event_loop()\n        self.time_getter = time_getter or time.time\n\n    async def run_and_return_one_or_none(self, query, *args):\n        for row in await self.db.execute_fetchall(query, args):\n            if len(row) == 1:\n                return row[0]\n            return row\n\n    async def run_and_return_list(self, query, *args):\n        rows = list(await self.db.execute_fetchall(query, args))\n        return [col[0] for col in rows] if rows else []\n\n    # # # # # # # # # blob functions # # # # # # # # #\n\n    async def add_blobs(self, *blob_hashes_and_lengths: typing.Tuple[str, int, int, int], finished=False):\n        def _add_blobs(transaction: sqlite3.Connection):\n            transaction.executemany(\n                \"insert or ignore into blob values (?, ?, ?, ?, ?, ?, ?, ?, ?)\",\n                (\n                    (blob_hash, length, 0, 0, \"pending\" if not finished else \"finished\", 0, 0, added_on, is_mine)\n                    for blob_hash, length, added_on, is_mine in blob_hashes_and_lengths\n                )\n            ).fetchall()\n            if finished:\n                transaction.executemany(\n                    \"update blob set status='finished' where blob.blob_hash=?\", (\n                        (blob_hash, ) for blob_hash, _, _, _ in blob_hashes_and_lengths\n                    )\n                ).fetchall()\n        return await self.db.run(_add_blobs)\n\n    def get_blob_status(self, blob_hash: str):\n        return self.run_and_return_one_or_none(\n            \"select status from blob where blob_hash=?\", blob_hash\n        )\n\n    def set_announce(self, *blob_hashes):\n        return self.db.execute_fetchall(\n            \"update blob set should_announce=1 where blob_hash in (?, ?)\", blob_hashes\n        )\n\n    def update_last_announced_blobs(self, blob_hashes: typing.List[str]):\n        def _update_last_announced_blobs(transaction: sqlite3.Connection):\n            last_announced = self.time_getter()\n            return transaction.executemany(\n                \"update blob set next_announce_time=?, last_announced_time=?, single_announce=0 \"\n                \"where blob_hash=?\",\n                ((int(last_announced + (DATA_EXPIRATION / 2)), int(last_announced), blob_hash)\n                 for blob_hash in blob_hashes)\n            ).fetchall()\n        return self.db.run(_update_last_announced_blobs)\n\n    def should_single_announce_blobs(self, blob_hashes, immediate=False):\n        def set_single_announce(transaction):\n            now = int(self.time_getter())\n            for blob_hash in blob_hashes:\n                if immediate:\n                    transaction.execute(\n                        \"update blob set single_announce=1, next_announce_time=? \"\n                        \"where blob_hash=? and status='finished'\", (int(now), blob_hash)\n                    ).fetchall()\n                else:\n                    transaction.execute(\n                        \"update blob set single_announce=1 where blob_hash=? and status='finished'\", (blob_hash,)\n                    ).fetchall()\n        return self.db.run(set_single_announce)\n\n    def get_blobs_to_announce(self):\n        def get_and_update(transaction):\n            timestamp = int(self.time_getter())\n            if self.conf.announce_head_and_sd_only:\n                r = transaction.execute(\n                    \"select blob_hash from blob \"\n                    \"where blob_hash is not null and \"\n                    \"(should_announce=1 or single_announce=1) and next_announce_time<? and status='finished' \"\n                    \"order by next_announce_time asc limit ?\",\n                    (timestamp, int(self.conf.concurrent_blob_announcers * 10))\n                ).fetchall()\n            else:\n                r = transaction.execute(\n                    \"select blob_hash from blob where blob_hash is not null \"\n                    \"and next_announce_time<? and status='finished' \"\n                    \"order by next_announce_time asc limit ?\",\n                    (timestamp, int(self.conf.concurrent_blob_announcers * 10))\n                ).fetchall()\n            return [b[0] for b in r]\n        return self.db.run(get_and_update)\n\n    def delete_blobs_from_db(self, blob_hashes):\n        def delete_blobs(transaction):\n            transaction.executemany(\n                \"delete from blob where blob_hash=?;\", ((blob_hash,) for blob_hash in blob_hashes)\n            ).fetchall()\n        return self.db.run_with_foreign_keys_disabled(delete_blobs)\n\n    def get_all_blob_hashes(self):\n        return self.run_and_return_list(\"select blob_hash from blob\")\n\n    async def get_stored_blobs(self, is_mine: bool, is_network_blob=False):\n        is_mine = 1 if is_mine else 0\n        if is_network_blob:\n            return await self.db.execute_fetchall(\n                \"select blob.blob_hash, blob.blob_length, blob.added_on \"\n                \"from blob left join stream_blob using (blob_hash) \"\n                \"where stream_blob.stream_hash is null and blob.is_mine=? and blob.status='finished'\"\n                \"order by blob.blob_length desc, blob.added_on asc\",\n                (is_mine,)\n            )\n\n        sd_blobs = await self.db.execute_fetchall(\n            \"select blob.blob_hash, blob.blob_length, blob.added_on \"\n            \"from blob join stream on blob.blob_hash=stream.sd_hash join file using (stream_hash) \"\n            \"where blob.is_mine=? order by blob.added_on asc\",\n            (is_mine,)\n        )\n        content_blobs = await self.db.execute_fetchall(\n            \"select blob.blob_hash, blob.blob_length, blob.added_on \"\n            \"from blob join stream_blob using (blob_hash) cross join stream using (stream_hash)\"\n            \"cross join file using (stream_hash)\"\n            \"where blob.is_mine=? and blob.status='finished' order by blob.added_on asc, blob.blob_length asc\",\n            (is_mine,)\n        )\n        return content_blobs + sd_blobs\n\n    async def get_stored_blob_disk_usage(self):\n        total, network_size, content_size, private_size = await self.db.execute_fetchone(\"\"\"\n        select coalesce(sum(blob_length), 0) as total,\n               coalesce(sum(case when\n                   stream_blob.stream_hash is null\n               then blob_length else 0 end), 0) as network_storage,\n               coalesce(sum(case when\n                   stream_blob.blob_hash is not null and is_mine=0\n               then blob_length else 0 end), 0) as content_storage,\n               coalesce(sum(case when\n                   is_mine=1\n               then blob_length else 0 end), 0) as private_storage\n        from blob left join stream_blob using (blob_hash)\n        where blob_hash not in (select sd_hash from stream) and blob.status=\"finished\"\n        \"\"\")\n        return {\n            'network_storage': network_size,\n            'content_storage': content_size,\n            'private_storage': private_size,\n            'total': total\n        }\n\n    async def update_blob_ownership(self, sd_hash, is_mine: bool):\n        is_mine = 1 if is_mine else 0\n        await self.db.execute_fetchall(\n            \"update blob set is_mine = ? where blob_hash in (\"\n            \"   select blob_hash from blob natural join stream_blob natural join stream where sd_hash = ?\"\n            \") OR blob_hash = ?\", (is_mine, sd_hash, sd_hash)\n        )\n\n    def sync_missing_blobs(self, blob_files: typing.Set[str]) -> typing.Awaitable[typing.Set[str]]:\n        def _sync_blobs(transaction: sqlite3.Connection) -> typing.Set[str]:\n            finished_blob_hashes = tuple(\n                blob_hash for (blob_hash, ) in transaction.execute(\n                    \"select blob_hash from blob where status='finished'\"\n                ).fetchall()\n            )\n            finished_blobs_set = set(finished_blob_hashes)\n            to_update_set = finished_blobs_set.difference(blob_files)\n            transaction.executemany(\n                \"update blob set status='pending' where blob_hash=?\",\n                ((blob_hash, ) for blob_hash in to_update_set)\n            ).fetchall()\n            return blob_files.intersection(finished_blobs_set)\n        return self.db.run(_sync_blobs)\n\n    # # # # # # # # # stream functions # # # # # # # # #\n\n    async def stream_exists(self, sd_hash: str) -> bool:\n        streams = await self.run_and_return_one_or_none(\"select stream_hash from stream where sd_hash=?\", sd_hash)\n        return streams is not None\n\n    async def file_exists(self, sd_hash: str) -> bool:\n        streams = await self.run_and_return_one_or_none(\"select f.stream_hash from file f \"\n                                                        \"inner join stream s on \"\n                                                        \"s.stream_hash=f.stream_hash and s.sd_hash=?\", sd_hash)\n        return streams is not None\n\n    def store_stream(self, sd_blob: 'BlobFile', descriptor: 'StreamDescriptor'):\n        return self.db.run(store_stream, sd_blob, descriptor)\n\n    def get_blobs_for_stream(self, stream_hash, only_completed=False) -> typing.Awaitable[typing.List[BlobInfo]]:\n        def _get_blobs_for_stream(transaction):\n            crypt_blob_infos = []\n            stream_blobs = transaction.execute(\n                \"select s.blob_hash, s.position, s.iv, b.added_on \"\n                \"from stream_blob s left outer join blob b on b.blob_hash=s.blob_hash where stream_hash=? \"\n                \"order by position asc\", (stream_hash, )\n            ).fetchall()\n            if only_completed:\n                lengths = transaction.execute(\n                    \"select b.blob_hash, b.blob_length from blob b \"\n                    \"inner join stream_blob s ON b.blob_hash=s.blob_hash and b.status='finished' and s.stream_hash=?\",\n                    (stream_hash, )\n                ).fetchall()\n            else:\n                lengths = transaction.execute(\n                    \"select b.blob_hash, b.blob_length from blob b \"\n                    \"inner join stream_blob s ON b.blob_hash=s.blob_hash and s.stream_hash=?\",\n                    (stream_hash, )\n                ).fetchall()\n\n            blob_length_dict = {}\n            for blob_hash, length in lengths:\n                blob_length_dict[blob_hash] = length\n\n            current_time = time.time()\n            for blob_hash, position, iv, added_on in stream_blobs:\n                blob_length = blob_length_dict.get(blob_hash, 0)\n                crypt_blob_infos.append(BlobInfo(position, blob_length, iv, added_on or current_time, blob_hash))\n                if not blob_hash:\n                    break\n            return crypt_blob_infos\n        return self.db.run(_get_blobs_for_stream)\n\n    def get_sd_blob_hash_for_stream(self, stream_hash):\n        return self.run_and_return_one_or_none(\n            \"select sd_hash from stream where stream_hash=?\", stream_hash\n        )\n\n    def get_stream_hash_for_sd_hash(self, sd_blob_hash):\n        return self.run_and_return_one_or_none(\n            \"select stream_hash from stream where sd_hash = ?\", sd_blob_hash\n        )\n\n    def delete_stream(self, descriptor: 'StreamDescriptor'):\n        return self.db.run_with_foreign_keys_disabled(delete_stream, descriptor)\n\n    async def delete_torrent(self, bt_infohash: str):\n        return await self.db.run(delete_torrent, bt_infohash)\n\n    # # # # # # # # # file stuff # # # # # # # # #\n\n    def save_downloaded_file(self, stream_hash: str, file_name: typing.Optional[str],\n                             download_directory: typing.Optional[str], data_payment_rate: float,\n                             content_fee: typing.Optional[Transaction] = None,\n                             added_on: typing.Optional[int] = None) -> typing.Awaitable[int]:\n        return self.save_published_file(\n            stream_hash, file_name, download_directory, data_payment_rate, status=\"running\",\n            content_fee=content_fee, added_on=added_on\n        )\n\n    def save_published_file(self, stream_hash: str, file_name: typing.Optional[str],\n                            download_directory: typing.Optional[str], data_payment_rate: float,\n                            status: str = \"finished\",\n                            content_fee: typing.Optional[Transaction] = None,\n                            added_on: typing.Optional[int] = None) -> typing.Awaitable[int]:\n        return self.db.run(store_file, stream_hash, file_name, download_directory, data_payment_rate, status,\n                           content_fee, added_on)\n\n    async def update_manually_removed_files_since_last_run(self):\n        \"\"\"\n        Update files that have been removed from the downloads directory since the last run\n        \"\"\"\n        def update_manually_removed_files(transaction: sqlite3.Connection):\n            files = {}\n            query = \"select stream_hash, download_directory, file_name from file where saved_file=1 \" \\\n                    \"and stream_hash is not null\"\n            for (stream_hash, download_directory, file_name) in transaction.execute(query).fetchall():\n                if download_directory and file_name:\n                    files[stream_hash] = download_directory, file_name\n            return files\n\n        def detect_removed(files):\n            return [\n                stream_hash for stream_hash, (download_directory, file_name) in files.items()\n                if not os.path.isfile(os.path.join(binascii.unhexlify(download_directory).decode(),\n                                                   binascii.unhexlify(file_name).decode()))\n            ]\n\n        def update_db_removed(transaction: sqlite3.Connection, removed):\n            query = \"update file set file_name=null, download_directory=null, saved_file=0 where stream_hash in {}\"\n            for cur in _batched_select(transaction, query, removed):\n                cur.fetchall()\n\n        stream_and_file = await self.db.run(update_manually_removed_files)\n        removed = await self.loop.run_in_executor(None, detect_removed, stream_and_file)\n        if removed:\n            await self.db.run(update_db_removed, removed)\n\n    def get_all_lbry_files(self) -> typing.Awaitable[typing.List[typing.Dict]]:\n        return self.db.run(get_all_lbry_files)\n\n    def change_file_status(self, stream_hash: str, new_status: str):\n        log.debug(\"update file status %s -> %s\", stream_hash, new_status)\n        return self.db.execute_fetchall(\"update file set status=? where stream_hash=?\", (new_status, stream_hash))\n\n    def stop_all_files(self):\n        log.debug(\"stopping all files\")\n        return self.db.execute_fetchall(\"update file set status=?\", (\"stopped\",))\n\n    async def change_file_download_dir_and_file_name(self, stream_hash: str, download_dir: typing.Optional[str],\n                                                     file_name: typing.Optional[str]):\n        if not file_name or not download_dir:\n            encoded_file_name, encoded_download_dir = None, None\n        else:\n            encoded_file_name = binascii.hexlify(file_name.encode()).decode()\n            encoded_download_dir = binascii.hexlify(download_dir.encode()).decode()\n        return await self.db.execute_fetchall(\"update file set download_directory=?, file_name=? where stream_hash=?\", (\n            encoded_download_dir, encoded_file_name, stream_hash,\n        ))\n\n    async def save_content_fee(self, stream_hash: str, content_fee: Transaction):\n        return await self.db.execute_fetchall(\"update file set content_fee=? where stream_hash=?\", (\n            binascii.hexlify(content_fee.raw), stream_hash,\n        ))\n\n    async def set_saved_file(self, stream_hash: str):\n        return await self.db.execute_fetchall(\"update file set saved_file=1 where stream_hash=?\", (\n            stream_hash,\n        ))\n\n    async def clear_saved_file(self, stream_hash: str):\n        return await self.db.execute_fetchall(\"update file set saved_file=0 where stream_hash=?\", (\n            stream_hash,\n        ))\n\n    async def recover_streams(self, descriptors_and_sds: typing.List[typing.Tuple['StreamDescriptor', 'BlobFile',\n                                                                                  typing.Optional[Transaction]]],\n                              download_directory: str):\n        def _recover(transaction: sqlite3.Connection):\n            stream_hashes = [x[0].stream_hash for x in descriptors_and_sds]\n            for descriptor, sd_blob, content_fee in descriptors_and_sds:\n                content_claim = transaction.execute(\n                    \"select * from content_claim where stream_hash=?\", (descriptor.stream_hash, )\n                ).fetchone()\n                delete_stream(transaction, descriptor)  # this will also delete the content claim\n                store_stream(transaction, sd_blob, descriptor)\n                store_file(transaction, descriptor.stream_hash, os.path.basename(descriptor.suggested_file_name),\n                           download_directory, 0.0, 'stopped', content_fee=content_fee)\n                if content_claim:\n                    transaction.execute(\"insert or ignore into content_claim values (?, ?, ?)\", content_claim)\n            transaction.executemany(\n                \"update file set status='stopped' where stream_hash=?\",\n                ((stream_hash, ) for stream_hash in stream_hashes)\n            ).fetchall()\n            download_dir = binascii.hexlify(self.conf.download_dir.encode()).decode()\n            transaction.executemany(\n                \"update file set download_directory=? where stream_hash=?\",\n                ((download_dir, stream_hash) for stream_hash in stream_hashes)\n            ).fetchall()\n        await self.db.run_with_foreign_keys_disabled(_recover)\n\n    def get_all_stream_hashes(self):\n        return self.run_and_return_list(\"select stream_hash from stream\")\n\n    # # # # # # # # # support functions # # # # # # # # #\n\n    def save_supports(self, claim_id_to_supports: dict):\n        # TODO: add 'address' to support items returned for a claim from lbrycrdd and lbryum-server\n        def _save_support(transaction):\n            bind = \"({})\".format(','.join(['?'] * len(claim_id_to_supports)))\n            transaction.execute(\n                f\"delete from support where claim_id in {bind}\", tuple(claim_id_to_supports.keys())\n            ).fetchall()\n            for claim_id, supports in claim_id_to_supports.items():\n                for support in supports:\n                    transaction.execute(\n                        \"insert into support values (?, ?, ?, ?)\",\n                        (\"%s:%i\" % (support['txid'], support['nout']), claim_id, lbc_to_dewies(support['amount']),\n                         support.get('address', \"\"))\n                    ).fetchall()\n        return self.db.run(_save_support)\n\n    def get_supports(self, *claim_ids):\n        def _format_support(outpoint, supported_id, amount, address):\n            return {\n                \"txid\": outpoint.split(\":\")[0],\n                \"nout\": int(outpoint.split(\":\")[1]),\n                \"claim_id\": supported_id,\n                \"amount\": dewies_to_lbc(amount),\n                \"address\": address,\n            }\n\n        def _get_supports(transaction):\n            return [\n                _format_support(*support_info)\n                for support_info in _batched_select(\n                    transaction,\n                    \"select * from support where claim_id in {}\",\n                    claim_ids\n                )\n            ]\n\n        return self.db.run(_get_supports)\n\n    # # # # # # # # # claim functions # # # # # # # # #\n\n    async def save_claims(self, claim_infos):\n        claim_id_to_supports = {}\n        update_file_callbacks = []\n\n        def _save_claims(transaction):\n            content_claims_to_update = []\n            for claim_info in claim_infos:\n                outpoint = \"%s:%i\" % (claim_info['txid'], claim_info['nout'])\n                claim_id = claim_info['claim_id']\n                name = claim_info['name']\n                amount = lbc_to_dewies(claim_info['amount'])\n                height = claim_info['height']\n                address = claim_info['address']\n                sequence = claim_info['claim_sequence']\n                certificate_id = claim_info['value'].signing_channel_id\n                try:\n                    source_hash = claim_info['value'].stream.source.sd_hash\n                except (AttributeError, ValueError):\n                    source_hash = None\n                serialized = binascii.hexlify(claim_info['value'].to_bytes())\n                transaction.execute(\n                    \"insert or replace into claim values (?, ?, ?, ?, ?, ?, ?, ?, ?)\",\n                    (outpoint, claim_id, name, amount, height, serialized, certificate_id, address, sequence)\n                ).fetchall()\n                # if this response doesn't have support info don't overwrite the existing\n                # support info\n                if 'supports' in claim_info:\n                    claim_id_to_supports[claim_id] = claim_info['supports']\n                if not source_hash:\n                    continue\n                stream_hash = transaction.execute(\n                    \"select file.stream_hash from stream \"\n                    \"inner join file on file.stream_hash=stream.stream_hash where sd_hash=?\", (source_hash,)\n                ).fetchone()\n                if not stream_hash:\n                    continue\n                stream_hash = stream_hash[0]\n                known_outpoint = transaction.execute(\n                    \"select claim_outpoint from content_claim where stream_hash=?\", (stream_hash,)\n                ).fetchone()\n                known_claim_id = transaction.execute(\n                    \"select claim_id from claim \"\n                    \"inner join content_claim c3 ON claim.claim_outpoint=c3.claim_outpoint \"\n                    \"where c3.stream_hash=?\", (stream_hash,)\n                ).fetchone()\n                if not known_claim_id:\n                    content_claims_to_update.append((stream_hash, outpoint))\n                elif known_outpoint != outpoint:\n                    content_claims_to_update.append((stream_hash, outpoint))\n            for stream_hash, outpoint in content_claims_to_update:\n                self._save_content_claim(transaction, outpoint, stream_hash)\n                if stream_hash in self.content_claim_callbacks:\n                    update_file_callbacks.append(self.content_claim_callbacks[stream_hash]())\n\n        await self.db.run(_save_claims)\n        if update_file_callbacks:\n            await asyncio.wait(map(asyncio.create_task, update_file_callbacks))\n        if claim_id_to_supports:\n            await self.save_supports(claim_id_to_supports)\n\n    def save_claim_from_output(self, ledger, *outputs: Output):\n        return self.save_claims([{\n            \"claim_id\": output.claim_id,\n            \"name\": output.claim_name,\n            \"amount\": dewies_to_lbc(output.amount),\n            \"address\": output.get_address(ledger),\n            \"txid\": output.tx_ref.id,\n            \"nout\": output.position,\n            \"value\": output.claim,\n            \"height\": output.tx_ref.height,\n            \"claim_sequence\": -1,\n        } for output in outputs])\n\n    def save_claims_for_resolve(self, claim_infos):\n        to_save = {}\n        for info in claim_infos:\n            if 'value' in info:\n                if info['value']:\n                    to_save[info['claim_id']] = info\n            else:\n                for key in ('certificate', 'claim'):\n                    if info.get(key, {}).get('value'):\n                        to_save[info[key]['claim_id']] = info[key]\n        return self.save_claims(to_save.values())\n\n    @staticmethod\n    def _save_content_claim(transaction, claim_outpoint, stream_hash=None, bt_infohash=None):\n        assert stream_hash or bt_infohash\n        # get the claim id and serialized metadata\n        claim_info = transaction.execute(\n            \"select claim_id, serialized_metadata from claim where claim_outpoint=?\", (claim_outpoint,)\n        ).fetchone()\n        if not claim_info:\n            raise Exception(\"claim not found\")\n        new_claim_id, claim = claim_info[0], Claim.from_bytes(binascii.unhexlify(claim_info[1]))\n\n        # certificate claims should not be in the content_claim table\n        if not claim.is_stream:\n            raise Exception(\"claim does not contain a stream\")\n\n        # get the known sd hash for this stream\n        known_sd_hash = transaction.execute(\n            \"select sd_hash from stream where stream_hash=?\", (stream_hash,)\n        ).fetchone()\n        if not known_sd_hash:\n            raise Exception(\"stream not found\")\n        # check the claim contains the same sd hash\n        if known_sd_hash[0] != claim.stream.source.sd_hash:\n            raise Exception(\"stream mismatch\")\n\n        # if there is a current claim associated to the file, check that the new claim is an update to it\n        current_associated_content = transaction.execute(\n            \"select claim_outpoint from content_claim where stream_hash=?\", (stream_hash,)\n        ).fetchone()\n        if current_associated_content:\n            current_associated_claim_id = transaction.execute(\n                \"select claim_id from claim where claim_outpoint=?\", current_associated_content\n            ).fetchone()[0]\n            if current_associated_claim_id != new_claim_id:\n                raise Exception(\n                    f\"mismatching claim ids when updating stream {current_associated_claim_id} vs {new_claim_id}\"\n                )\n\n        # update the claim associated to the file\n        transaction.execute(\"delete from content_claim where stream_hash=?\", (stream_hash, )).fetchall()\n        transaction.execute(\n            \"insert into content_claim values (?, NULL, ?)\", (stream_hash, claim_outpoint)\n        ).fetchall()\n\n    async def save_content_claim(self, stream_hash, claim_outpoint):\n        await self.db.run(self._save_content_claim, claim_outpoint, stream_hash)\n        # update corresponding ManagedEncryptedFileDownloader object\n        if stream_hash in self.content_claim_callbacks:\n            await self.content_claim_callbacks[stream_hash]()\n\n    async def save_torrent_content_claim(self, bt_infohash, claim_outpoint, length, name):\n        def _save_torrent(transaction):\n            transaction.execute(\n                \"insert or replace into torrent values (?, NULL, ?, ?)\", (bt_infohash, length, name)\n            ).fetchall()\n            transaction.execute(\n                \"insert or replace into content_claim values (NULL, ?, ?)\", (bt_infohash, claim_outpoint)\n            ).fetchall()\n        await self.db.run(_save_torrent)\n        # update corresponding ManagedEncryptedFileDownloader object\n        if bt_infohash in self.content_claim_callbacks:\n            await self.content_claim_callbacks[bt_infohash]()\n\n    async def get_content_claim(self, stream_hash: str, include_supports: typing.Optional[bool] = True) -> typing.Dict:\n        claims = await self.db.run(get_claims_from_stream_hashes, [stream_hash])\n        claim = None\n        if claims:\n            claim = claims[stream_hash].as_dict()\n            if include_supports:\n                supports = await self.get_supports(claim['claim_id'])\n                claim['supports'] = supports\n                claim['effective_amount'] = calculate_effective_amount(claim['amount'], supports)\n        return claim\n\n    async def get_content_claim_for_torrent(self, bt_infohash):\n        claims = await self.db.run(get_claims_from_torrent_info_hashes, [bt_infohash])\n        return claims[bt_infohash].as_dict() if claims else None\n\n    # # # # # # # # # reflector functions # # # # # # # # #\n\n    def update_reflected_stream(self, sd_hash, reflector_address, success=True):\n        if success:\n            return self.db.execute_fetchall(\n                \"insert or replace into reflected_stream values (?, ?, ?)\",\n                (sd_hash, reflector_address, self.time_getter())\n            )\n        return self.db.execute_fetchall(\n            \"delete from reflected_stream where sd_hash=? and reflector_address=?\",\n            (sd_hash, reflector_address)\n        )\n\n    def get_streams_to_re_reflect(self):\n        return self.run_and_return_list(\n            \"select s.sd_hash from stream s \"\n            \"left outer join reflected_stream r on s.sd_hash=r.sd_hash \"\n            \"where r.timestamp is null or r.timestamp < ?\",\n            int(self.time_getter()) - 86400\n        )\n\n    # # # # # # # # # # dht functions # # # # # # # # # # #\n    async def get_persisted_kademlia_peers(self) -> typing.List[typing.Tuple[bytes, str, int, int]]:\n        query = 'select node_id, address, udp_port, tcp_port from peer'\n        return [(binascii.unhexlify(n), a, u, t) for n, a, u, t in await self.db.execute_fetchall(query)]\n\n    async def save_kademlia_peers(self, peers: typing.List['KademliaPeer']):\n        def _save_kademlia_peers(transaction: sqlite3.Connection):\n            transaction.execute('delete from peer').fetchall()\n            transaction.executemany(\n                'insert into peer(node_id, address, udp_port, tcp_port) values (?, ?, ?, ?)',\n                ((binascii.hexlify(p.node_id), p.address, p.udp_port, p.tcp_port) for p in peers)\n            ).fetchall()\n        return await self.db.run(_save_kademlia_peers)\n"
  },
  {
    "path": "lbry/extras/daemon/undecorated.py",
    "content": "# Copyright 2016-2017 Ionuț Arțăriși <ionut@artarisi.eu>\n\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n\n#     http://www.apache.org/licenses/LICENSE-2.0\n\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\n# This came from https://github.com/mapleoin/undecorated\n\nfrom inspect import isfunction, ismethod, isclass\n\n__version__ = '0.3.0'\n\n\ndef undecorated(o):\n    \"\"\"Remove all decorators from a function, method or class\"\"\"\n    # class decorator\n    if isinstance(o, type):\n        return o\n\n    try:\n        # python2\n        closure = o.func_closure\n    except AttributeError:\n        pass\n\n    try:\n        # python3\n        closure = o.__closure__\n    except AttributeError:\n        return\n\n    if closure:\n        for cell in closure:\n            # avoid infinite recursion\n            if cell.cell_contents is o:\n                continue\n\n            # check if the contents looks like a decorator; in that case\n            # we need to go one level down into the dream, otherwise it\n            # might just be a different closed-over variable, which we\n            # can ignore.\n\n            # Note: this favors supporting decorators defined without\n            # @wraps to the detriment of function/method/class closures\n            if looks_like_a_decorator(cell.cell_contents):\n                undecd = undecorated(cell.cell_contents)\n                if undecd:\n                    return undecd\n    return o\n\n\ndef looks_like_a_decorator(a):\n    return isfunction(a) or ismethod(a) or isclass(a)\n"
  },
  {
    "path": "lbry/extras/system_info.py",
    "content": "import platform\nimport os\nimport logging.handlers\n\nfrom lbry import build_info, __version__ as lbrynet_version\n\nlog = logging.getLogger(__name__)\n\n\ndef get_platform() -> dict:\n    os_system = platform.system()\n    if os.environ and 'ANDROID_ARGUMENT' in os.environ:\n        os_system = 'android'\n    d = {\n        \"processor\": platform.processor(),\n        \"python_version\": platform.python_version(),\n        \"platform\": platform.platform(),\n        \"os_release\": platform.release(),\n        \"os_system\": os_system,\n        \"lbrynet_version\": lbrynet_version,\n        \"version\": lbrynet_version,\n        \"build\": build_info.BUILD,  # CI server sets this during build step\n    }\n    if d[\"os_system\"] == \"Linux\":\n        import distro  # pylint: disable=import-outside-toplevel\n        d[\"distro\"] = distro.info()\n        d[\"desktop\"] = os.environ.get('XDG_CURRENT_DESKTOP', 'Unknown')\n\n    return d\n"
  },
  {
    "path": "lbry/file/__init__.py",
    "content": ""
  },
  {
    "path": "lbry/file/file_manager.py",
    "content": "import asyncio\nimport logging\nimport typing\nfrom typing import Optional\nfrom aiohttp.web import Request\nfrom lbry.error import ResolveError, DownloadSDTimeoutError, InsufficientFundsError\nfrom lbry.error import ResolveTimeoutError, DownloadDataTimeoutError, KeyFeeAboveMaxAllowedError\nfrom lbry.error import InvalidStreamURLError\nfrom lbry.stream.managed_stream import ManagedStream\nfrom lbry.torrent.torrent_manager import TorrentSource\nfrom lbry.utils import cache_concurrent\nfrom lbry.schema.url import URL\nfrom lbry.wallet.dewies import dewies_to_lbc\nfrom lbry.file.source_manager import SourceManager\nfrom lbry.file.source import ManagedDownloadSource\nfrom lbry.extras.daemon.storage import StoredContentClaim\nif typing.TYPE_CHECKING:\n    from lbry.conf import Config\n    from lbry.extras.daemon.analytics import AnalyticsManager\n    from lbry.extras.daemon.storage import SQLiteStorage\n    from lbry.wallet import WalletManager\n    from lbry.extras.daemon.exchange_rate_manager import ExchangeRateManager\n\nlog = logging.getLogger(__name__)\n\n\nclass FileManager:\n    def __init__(self, loop: asyncio.AbstractEventLoop, config: 'Config', wallet_manager: 'WalletManager',\n                 storage: 'SQLiteStorage', analytics_manager: Optional['AnalyticsManager'] = None):\n        self.loop = loop\n        self.config = config\n        self.wallet_manager = wallet_manager\n        self.storage = storage\n        self.analytics_manager = analytics_manager\n        self.source_managers: typing.Dict[str, SourceManager] = {}\n        self.started = asyncio.Event()\n\n    @property\n    def streams(self):\n        return self.source_managers['stream']._sources\n\n    async def create_stream(self, file_path: str, key: Optional[bytes] = None, **kwargs) -> ManagedDownloadSource:\n        if 'stream' in self.source_managers:\n            return await self.source_managers['stream'].create(file_path, key, **kwargs)\n        raise NotImplementedError\n\n    async def start(self):\n        await asyncio.gather(*(source_manager.start() for source_manager in self.source_managers.values()))\n        for manager in self.source_managers.values():\n            await manager.started.wait()\n        self.started.set()\n\n    async def stop(self):\n        for manager in self.source_managers.values():\n            # fixme: pop or not?\n            await manager.stop()\n        self.started.clear()\n\n    @cache_concurrent\n    async def download_from_uri(self, uri, exchange_rate_manager: 'ExchangeRateManager',\n                                timeout: Optional[float] = None, file_name: Optional[str] = None,\n                                download_directory: Optional[str] = None,\n                                save_file: Optional[bool] = None, resolve_timeout: float = 3.0,\n                                wallet: Optional['Wallet'] = None) -> ManagedDownloadSource:\n\n        wallet = wallet or self.wallet_manager.default_wallet\n        timeout = timeout or self.config.download_timeout\n        start_time = self.loop.time()\n        resolved_time = None\n        stream = None\n        claim = None\n        error = None\n        outpoint = None\n        if save_file is None:\n            save_file = self.config.save_files\n        if file_name and not save_file:\n            save_file = True\n        if save_file:\n            download_directory = download_directory or self.config.download_dir\n        else:\n            download_directory = None\n\n        payment = None\n        try:\n            # resolve the claim\n            try:\n                if not URL.parse(uri).has_stream:\n                    raise InvalidStreamURLError(uri)\n            except ValueError:\n                raise InvalidStreamURLError(uri)\n            try:\n                resolved_result = await asyncio.wait_for(\n                    self.wallet_manager.ledger.resolve(\n                        wallet.accounts, [uri],\n                        include_purchase_receipt=True,\n                        include_is_my_output=True\n                    ), resolve_timeout\n                )\n            except asyncio.TimeoutError:\n                raise ResolveTimeoutError(uri)\n            except Exception as err:\n                log.exception(\"Unexpected error resolving stream:\")\n                raise ResolveError(f\"Unexpected error resolving stream: {str(err)}\")\n            if 'error' in resolved_result:\n                raise ResolveError(f\"Unexpected error resolving uri for download: {resolved_result['error']}\")\n            if not resolved_result or uri not in resolved_result:\n                raise ResolveError(f\"Failed to resolve stream at '{uri}'\")\n            txo = resolved_result[uri]\n            if isinstance(txo, dict):\n                raise ResolveError(f\"Failed to resolve stream at '{uri}': {txo}\")\n            claim = txo.claim\n            outpoint = f\"{txo.tx_ref.id}:{txo.position}\"\n            resolved_time = self.loop.time() - start_time\n            await self.storage.save_claim_from_output(self.wallet_manager.ledger, txo)\n\n            ####################\n            # update or replace\n            ####################\n\n            if claim.stream.source.bt_infohash:\n                source_manager = self.source_managers['torrent']\n                existing = source_manager.get_filtered(bt_infohash=claim.stream.source.bt_infohash)\n            elif claim.stream.source.sd_hash:\n                source_manager = self.source_managers['stream']\n                existing = source_manager.get_filtered(sd_hash=claim.stream.source.sd_hash)\n            else:\n                raise ResolveError(f\"There is nothing to download at {uri} - Source is unknown or unset\")\n\n            # resume or update an existing stream, if the stream changed: download it and delete the old one after\n            to_replace, updated_stream = None, None\n            if existing and existing[0].claim_id != txo.claim_id:\n                raise ResolveError(f\"stream for {existing[0].claim_id} collides with existing download {txo.claim_id}\")\n            if existing:\n                log.info(\"claim contains a metadata only update to a stream we have\")\n                if claim.stream.source.bt_infohash:\n                    await self.storage.save_torrent_content_claim(\n                        existing[0].identifier, outpoint, existing[0].torrent_length, existing[0].torrent_name\n                    )\n                    claim_info = await self.storage.get_content_claim_for_torrent(existing[0].identifier)\n                    existing[0].set_claim(claim_info, claim)\n                else:\n                    await self.storage.save_content_claim(\n                        existing[0].stream_hash, outpoint\n                    )\n                    await source_manager._update_content_claim(existing[0])\n                updated_stream = existing[0]\n            else:\n                existing_for_claim_id = self.get_filtered(claim_id=txo.claim_id)\n                if existing_for_claim_id:\n                    log.info(\"claim contains an update to a stream we have, downloading it\")\n                    if save_file and existing_for_claim_id[0].output_file_exists:\n                        save_file = False\n                    if not claim.stream.source.bt_infohash:\n                        existing_for_claim_id[0].downloader.node = source_manager.node\n                    await existing_for_claim_id[0].start(timeout=timeout, save_now=save_file)\n                    if not existing_for_claim_id[0].output_file_exists and (\n                            save_file or file_name or download_directory):\n                        await existing_for_claim_id[0].save_file(\n                            file_name=file_name, download_directory=download_directory\n                        )\n                    to_replace = existing_for_claim_id[0]\n\n            # resume or update an existing stream, if the stream changed: download it and delete the old one after\n            if updated_stream:\n                log.info(\"already have stream for %s\", uri)\n                if save_file and updated_stream.output_file_exists:\n                    save_file = False\n                if not claim.stream.source.bt_infohash:\n                    updated_stream.downloader.node = source_manager.node\n                await updated_stream.start(timeout=timeout, save_now=save_file)\n                if not updated_stream.output_file_exists and (save_file or file_name or download_directory):\n                    await updated_stream.save_file(\n                        file_name=file_name, download_directory=download_directory\n                    )\n                return updated_stream\n\n            ####################\n            # pay fee\n            ####################\n\n            needs_purchasing = (\n                not to_replace and\n                not txo.is_my_output and\n                txo.has_price and\n                not txo.purchase_receipt\n            )\n\n            if needs_purchasing:\n                payment = await self.wallet_manager.create_purchase_transaction(\n                    wallet.accounts, txo, exchange_rate_manager\n                )\n\n            ####################\n            # make downloader and wait for start\n            ####################\n            # temporary with fields we know so downloader can start. Missing fields are populated later.\n            stored_claim = StoredContentClaim(outpoint=outpoint, claim_id=txo.claim_id, name=txo.claim_name,\n                                              amount=txo.amount, height=txo.tx_ref.height,\n                                              serialized=claim.to_bytes().hex())\n\n            if not claim.stream.source.bt_infohash:\n                # fixme: this shouldnt be here\n                stream = ManagedStream(\n                    self.loop, self.config, source_manager.blob_manager, claim.stream.source.sd_hash,\n                    download_directory, file_name, ManagedStream.STATUS_RUNNING, content_fee=payment,\n                    analytics_manager=self.analytics_manager, claim=stored_claim\n                )\n                stream.downloader.node = source_manager.node\n            else:\n                stream = TorrentSource(\n                    self.loop, self.config, self.storage, identifier=claim.stream.source.bt_infohash,\n                    file_name=file_name, download_directory=download_directory or self.config.download_dir,\n                    status=ManagedStream.STATUS_RUNNING, claim=stored_claim, analytics_manager=self.analytics_manager,\n                    torrent_session=source_manager.torrent_session\n                )\n            log.info(\"starting download for %s\", uri)\n\n            before_download = self.loop.time()\n            await stream.start(timeout, save_file)\n\n            ####################\n            # success case: delete to_replace if applicable, broadcast fee payment\n            ####################\n\n            if to_replace:  # delete old stream now that the replacement has started downloading\n                await source_manager.delete(to_replace)\n\n            if payment is not None:\n                await self.wallet_manager.broadcast_or_release(payment)\n                payment = None  # to avoid releasing in `finally` later\n                log.info(\"paid fee of %s for %s\", dewies_to_lbc(stream.content_fee.outputs[0].amount), uri)\n                await self.storage.save_content_fee(stream.stream_hash, stream.content_fee)\n\n            source_manager.add(stream)\n\n            if not claim.stream.source.bt_infohash:\n                await self.storage.save_content_claim(stream.stream_hash, outpoint)\n            else:\n                await self.storage.save_torrent_content_claim(\n                    stream.identifier, outpoint, stream.torrent_length, stream.torrent_name\n                )\n                claim_info = await self.storage.get_content_claim_for_torrent(stream.identifier)\n                stream.set_claim(claim_info, claim)\n            if save_file:\n                await asyncio.wait_for(stream.save_file(), timeout - (self.loop.time() - before_download))\n            return stream\n        except asyncio.TimeoutError:\n            error = DownloadDataTimeoutError(stream.sd_hash)\n            raise error\n        except (Exception, asyncio.CancelledError) as err:  # forgive data timeout, don't delete stream\n            expected = (DownloadSDTimeoutError, DownloadDataTimeoutError, InsufficientFundsError,\n                        KeyFeeAboveMaxAllowedError, ResolveError, InvalidStreamURLError)\n            if isinstance(err, expected):\n                log.warning(\"Failed to download %s: %s\", uri, str(err))\n            elif isinstance(err, asyncio.CancelledError):\n                pass\n            else:\n                log.exception(\"Unexpected error downloading stream:\")\n            error = err\n            raise\n        finally:\n            if payment is not None:\n                # payment is set to None after broadcasting, if we're here an exception probably happened\n                await self.wallet_manager.ledger.release_tx(payment)\n            if self.analytics_manager and claim and claim.stream.source.bt_infohash:\n                # TODO: analytics for torrents\n                pass\n            elif self.analytics_manager and (error or (stream and (stream.downloader.time_to_descriptor or\n                                                                   stream.downloader.time_to_first_bytes))):\n                server = self.wallet_manager.ledger.network.client.server\n                self.loop.create_task(\n                    self.analytics_manager.send_time_to_first_bytes(\n                        resolved_time, self.loop.time() - start_time, None if not stream else stream.download_id,\n                        uri, outpoint,\n                        None if not stream else len(stream.downloader.blob_downloader.active_connections),\n                        None if not stream else len(stream.downloader.blob_downloader.scores),\n                        None if not stream else len(stream.downloader.blob_downloader.connection_failures),\n                        False if not stream else stream.downloader.added_fixed_peers,\n                        self.config.fixed_peer_delay if not stream else stream.downloader.fixed_peers_delay,\n                        None if not stream else stream.sd_hash,\n                        None if not stream else stream.downloader.time_to_descriptor,\n                        None if not (stream and stream.descriptor) else stream.descriptor.blobs[0].blob_hash,\n                        None if not (stream and stream.descriptor) else stream.descriptor.blobs[0].length,\n                        None if not stream else stream.downloader.time_to_first_bytes,\n                        None if not error else error.__class__.__name__,\n                        None if not error else str(error),\n                        None if not server else f\"{server[0]}:{server[1]}\"\n                    )\n                )\n\n    async def stream_partial_content(self, request: Request, sd_hash: str):\n        return await self.source_managers['stream'].stream_partial_content(request, sd_hash)\n\n    def get_filtered(self, *args, **kwargs) -> typing.List[ManagedDownloadSource]:\n        \"\"\"\n        Get a list of filtered and sorted ManagedStream objects\n\n        :param sort_by: field to sort by\n        :param reverse: reverse sorting\n        :param comparison: comparison operator used for filtering\n        :param search_by: fields and values to filter by\n        \"\"\"\n        return sum((manager.get_filtered(*args, **kwargs) for manager in self.source_managers.values()), [])\n\n    async def delete(self, source: ManagedDownloadSource, delete_file=False):\n        for manager in self.source_managers.values():\n            await manager.delete(source, delete_file)\n"
  },
  {
    "path": "lbry/file/source.py",
    "content": "import os\nimport asyncio\nimport typing\nimport logging\nimport binascii\nfrom typing import Optional\nfrom lbry.utils import generate_id\nfrom lbry.extras.daemon.storage import StoredContentClaim\n\nif typing.TYPE_CHECKING:\n    from lbry.conf import Config\n    from lbry.extras.daemon.analytics import AnalyticsManager\n    from lbry.wallet.transaction import Transaction\n    from lbry.extras.daemon.storage import SQLiteStorage\n\nlog = logging.getLogger(__name__)\n\n\nclass ManagedDownloadSource:\n    STATUS_RUNNING = \"running\"\n    STATUS_STOPPED = \"stopped\"\n    STATUS_FINISHED = \"finished\"\n\n    SAVING_ID = 1\n    STREAMING_ID = 2\n\n    def __init__(self, loop: asyncio.AbstractEventLoop, config: 'Config', storage: 'SQLiteStorage', identifier: str,\n                 file_name: Optional[str] = None, download_directory: Optional[str] = None,\n                 status: Optional[str] = STATUS_STOPPED, claim: Optional[StoredContentClaim] = None,\n                 download_id: Optional[str] = None, rowid: Optional[int] = None,\n                 content_fee: Optional['Transaction'] = None,\n                 analytics_manager: Optional['AnalyticsManager'] = None,\n                 added_on: Optional[int] = None):\n        self.loop = loop\n        self.storage = storage\n        self.config = config\n        self.identifier = identifier\n        self.download_directory = download_directory\n        self._file_name = file_name\n        self._status = status\n        self.stream_claim_info = claim\n        self.download_id = download_id or binascii.hexlify(generate_id()).decode()\n        self.rowid = rowid\n        self.content_fee = content_fee\n        self.purchase_receipt = None\n        self._added_on = added_on\n        self.analytics_manager = analytics_manager\n        self.downloader = None\n\n        self.saving = asyncio.Event()\n        self.finished_writing = asyncio.Event()\n        self.started_writing = asyncio.Event()\n        self.finished_write_attempt = asyncio.Event()\n\n    # @classmethod\n    # async def create(cls, loop: asyncio.AbstractEventLoop, config: 'Config', file_path: str,\n    #                  key: Optional[bytes] = None,\n    #                  iv_generator: Optional[typing.Generator[bytes, None, None]] = None) -> 'ManagedDownloadSource':\n    #     raise NotImplementedError()\n\n    async def start(self, timeout: Optional[float] = None, save_now: Optional[bool] = False):\n        raise NotImplementedError()\n\n    async def stop(self, finished: bool = False):\n        raise NotImplementedError()\n\n    async def save_file(self, file_name: Optional[str] = None, download_directory: Optional[str] = None):\n        raise NotImplementedError()\n\n    async def stop_tasks(self):\n        raise NotImplementedError()\n\n    def set_claim(self, claim_info: typing.Dict, claim: 'Claim'):\n        self.stream_claim_info = StoredContentClaim(\n            f\"{claim_info['txid']}:{claim_info['nout']}\", claim_info['claim_id'],\n            claim_info['name'], claim_info['amount'], claim_info['height'],\n            binascii.hexlify(claim.to_bytes()).decode(), claim.signing_channel_id, claim_info['address'],\n            claim_info['claim_sequence'], claim_info.get('channel_name')\n        )\n\n    # async def update_content_claim(self, claim_info: Optional[typing.Dict] = None):\n    #     if not claim_info:\n    #         claim_info = await self.blob_manager.storage.get_content_claim(self.stream_hash)\n    #     self.set_claim(claim_info, claim_info['value'])\n\n    @property\n    def file_name(self) -> Optional[str]:\n        return self._file_name\n\n    @property\n    def added_on(self) -> Optional[int]:\n        return self._added_on\n\n    @property\n    def status(self) -> str:\n        return self._status\n\n    @property\n    def completed(self):\n        raise NotImplementedError()\n\n    # @property\n    # def stream_url(self):\n    #     return f\"http://{self.config.streaming_host}:{self.config.streaming_port}/stream/{self.sd_hash}\n\n    @property\n    def finished(self) -> bool:\n        return self.status == self.STATUS_FINISHED\n\n    @property\n    def running(self) -> bool:\n        return self.status == self.STATUS_RUNNING\n\n    @property\n    def claim_id(self) -> Optional[str]:\n        return None if not self.stream_claim_info else self.stream_claim_info.claim_id\n\n    @property\n    def txid(self) -> Optional[str]:\n        return None if not self.stream_claim_info else self.stream_claim_info.txid\n\n    @property\n    def nout(self) -> Optional[int]:\n        return None if not self.stream_claim_info else self.stream_claim_info.nout\n\n    @property\n    def outpoint(self) -> Optional[str]:\n        return None if not self.stream_claim_info else self.stream_claim_info.outpoint\n\n    @property\n    def claim_height(self) -> Optional[int]:\n        return None if not self.stream_claim_info else self.stream_claim_info.height\n\n    @property\n    def channel_claim_id(self) -> Optional[str]:\n        return None if not self.stream_claim_info else self.stream_claim_info.channel_claim_id\n\n    @property\n    def channel_name(self) -> Optional[str]:\n        return None if not self.stream_claim_info else self.stream_claim_info.channel_name\n\n    @property\n    def claim_name(self) -> Optional[str]:\n        return None if not self.stream_claim_info else self.stream_claim_info.claim_name\n\n    @property\n    def metadata(self) -> Optional[typing.Dict]:\n        return None if not self.stream_claim_info else self.stream_claim_info.claim.stream.to_dict()\n\n    @property\n    def metadata_protobuf(self) -> bytes:\n        if self.stream_claim_info:\n            return binascii.hexlify(self.stream_claim_info.claim.to_bytes())\n\n    @property\n    def full_path(self) -> Optional[str]:\n        return os.path.join(self.download_directory, os.path.basename(self.file_name)) \\\n            if self.file_name and self.download_directory else None\n\n    @property\n    def output_file_exists(self):\n        return os.path.isfile(self.full_path) if self.full_path else False\n"
  },
  {
    "path": "lbry/file/source_manager.py",
    "content": "import os\nimport asyncio\nimport logging\nimport typing\nfrom typing import Optional\nfrom lbry.file.source import ManagedDownloadSource\nif typing.TYPE_CHECKING:\n    from lbry.conf import Config\n    from lbry.extras.daemon.analytics import AnalyticsManager\n    from lbry.extras.daemon.storage import SQLiteStorage\n\nlog = logging.getLogger(__name__)\n\nCOMPARISON_OPERATORS = {\n    'eq': lambda a, b: a == b,\n    'ne': lambda a, b: a != b,\n    'g': lambda a, b: a > b,\n    'l': lambda a, b: a < b,\n    'ge': lambda a, b: a >= b,\n    'le': lambda a, b: a <= b,\n}\n\n\nclass SourceManager:\n    filter_fields = {\n        'rowid',\n        'status',\n        'file_name',\n        'added_on',\n        'download_path',\n        'claim_name',\n        'claim_height',\n        'claim_id',\n        'outpoint',\n        'txid',\n        'nout',\n        'channel_claim_id',\n        'channel_name',\n        'completed'\n    }\n\n    set_filter_fields = {\n        \"claim_ids\": \"claim_id\",\n        \"channel_claim_ids\": \"channel_claim_id\",\n        \"outpoints\": \"outpoint\"\n    }\n\n    source_class = ManagedDownloadSource\n\n    def __init__(self, loop: asyncio.AbstractEventLoop, config: 'Config', storage: 'SQLiteStorage',\n                 analytics_manager: Optional['AnalyticsManager'] = None):\n        self.loop = loop\n        self.config = config\n        self.storage = storage\n        self.analytics_manager = analytics_manager\n        self._sources: typing.Dict[str, ManagedDownloadSource] = {}\n        self.started = asyncio.Event()\n\n    def add(self, source: ManagedDownloadSource):\n        self._sources[source.identifier] = source\n\n    async def remove(self, source: ManagedDownloadSource):\n        if source.identifier not in self._sources:\n            return\n        self._sources.pop(source.identifier)\n        await source.stop_tasks()\n\n    async def initialize_from_database(self):\n        raise NotImplementedError()\n\n    async def start(self):\n        await self.initialize_from_database()\n        self.started.set()\n\n    async def stop(self):\n        while self._sources:\n            _, source = self._sources.popitem()\n            await source.stop_tasks()\n        self.started.clear()\n\n    async def create(self, file_path: str, key: Optional[bytes] = None,\n                     iv_generator: Optional[typing.Generator[bytes, None, None]] = None) -> ManagedDownloadSource:\n        raise NotImplementedError()\n\n    async def delete(self, source: ManagedDownloadSource, delete_file: Optional[bool] = False):\n        await self.remove(source)\n        if delete_file and source.output_file_exists:\n            os.remove(source.full_path)\n\n    def get_filtered(self, sort_by: Optional[str] = None, reverse: Optional[bool] = False,\n                     comparison: Optional[str] = None, **search_by) -> typing.List[ManagedDownloadSource]:\n        \"\"\"\n        Get a list of filtered and sorted ManagedStream objects\n\n        :param sort_by: field to sort by\n        :param reverse: reverse sorting\n        :param comparison: comparison operator used for filtering\n        :param search_by: fields and values to filter by\n        \"\"\"\n        if sort_by and sort_by not in self.filter_fields:\n            raise ValueError(f\"'{sort_by}' is not a valid field to sort by\")\n        if comparison and comparison not in COMPARISON_OPERATORS:\n            raise ValueError(f\"'{comparison}' is not a valid comparison\")\n        if 'full_status' in search_by:\n            del search_by['full_status']\n\n        for search in search_by:\n            if search not in self.filter_fields:\n                raise ValueError(f\"'{search}' is not a valid search operation\")\n\n        compare_sets = {}\n        if isinstance(search_by.get('claim_id'), list):\n            compare_sets['claim_ids'] = search_by.pop('claim_id')\n        if isinstance(search_by.get('outpoint'), list):\n            compare_sets['outpoints'] = search_by.pop('outpoint')\n        if isinstance(search_by.get('channel_claim_id'), list):\n            compare_sets['channel_claim_ids'] = search_by.pop('channel_claim_id')\n\n        if search_by or compare_sets:\n            comparison = comparison or 'eq'\n            streams = []\n            for stream in self._sources.values():\n                if compare_sets and not all(\n                        getattr(stream, self.set_filter_fields[set_search]) in val\n                        for set_search, val in compare_sets.items()):\n                    continue\n                if search_by and not all(\n                        COMPARISON_OPERATORS[comparison](getattr(stream, search), val)\n                        for search, val in search_by.items()):\n                    continue\n                streams.append(stream)\n        else:\n            streams = list(self._sources.values())\n        if sort_by:\n            streams.sort(key=lambda s: getattr(s, sort_by) or \"\")\n            if reverse:\n                streams.reverse()\n        return streams\n"
  },
  {
    "path": "lbry/file_analysis.py",
    "content": "import asyncio\nimport json\nimport logging\nimport os\nimport pathlib\nimport platform\nimport re\nimport shlex\nimport shutil\nimport subprocess\nfrom math import ceil\n\nimport lbry.utils\nfrom lbry.conf import TranscodeConfig\n\nlog = logging.getLogger(__name__)\n\n\nclass VideoFileAnalyzer:\n\n    def _replace_or_pop_env(self, variable):\n        if variable + '_ORIG' in self._env_copy:\n            self._env_copy[variable] = self._env_copy[variable + '_ORIG']\n        else:\n            self._env_copy.pop(variable, None)\n\n    def __init__(self, conf: TranscodeConfig):\n        self._conf = conf\n        self._available_encoders = \"\"\n        self._ffmpeg_installed = None\n        self._which_ffmpeg = None\n        self._which_ffprobe = None\n        self._env_copy = dict(os.environ)\n        self._checked_ffmpeg = False\n        if lbry.utils.is_running_from_bundle():\n            # handle the situation where PyInstaller overrides our runtime environment:\n            self._replace_or_pop_env('LD_LIBRARY_PATH')\n\n    @staticmethod\n    def _execute(command, environment):\n        # log.debug(\"Executing: %s\", command)\n        try:\n            with subprocess.Popen(\n                    shlex.split(command) if platform.system() != 'Windows' else command,\n                    stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=environment\n            ) as process:\n                (stdout, stderr) = process.communicate()  # blocks until the process exits\n                return stdout.decode(errors='replace') + stderr.decode(errors='replace'), process.returncode\n        except subprocess.SubprocessError as e:\n            return str(e), -1\n\n    # This create_subprocess_exec call is broken in Windows Python 3.7, but it's prettier than what's here.\n    # The recommended fix is switching to ProactorEventLoop, but that breaks UDP in Linux Python 3.7.\n    # We work around that issue here by using run_in_executor. Check it again in Python 3.8.\n    async def _execute_ffmpeg(self, arguments):\n        arguments = self._which_ffmpeg + \" \" + arguments\n        return await asyncio.get_event_loop().run_in_executor(None, self._execute, arguments, self._env_copy)\n\n    async def _execute_ffprobe(self, arguments):\n        arguments = self._which_ffprobe + \" \" + arguments\n        return await asyncio.get_event_loop().run_in_executor(None, self._execute, arguments, self._env_copy)\n\n    async def _verify_executables(self):\n        try:\n            await self._execute_ffprobe(\"-version\")\n            version, code = await self._execute_ffmpeg(\"-version\")\n        except Exception as e:\n            code = -1\n            version = str(e)\n        if code != 0 or not version.startswith(\"ffmpeg\"):\n            log.warning(\"Unable to run ffmpeg, but it was requested. Code: %d; Message: %s\", code, version)\n            raise FileNotFoundError(\"Unable to locate or run ffmpeg or ffprobe. Please install FFmpeg \"\n                                    \"and ensure that it is callable via PATH or conf.ffmpeg_path\")\n        log.debug(\"Using %s at %s\", version.splitlines()[0].split(\" Copyright\")[0], self._which_ffmpeg)\n        return version\n\n    @staticmethod\n    def _which_ffmpeg_and_ffmprobe(path):\n        return shutil.which(\"ffmpeg\", path=path), shutil.which(\"ffprobe\", path=path)\n\n    async def _verify_ffmpeg_installed(self):\n        if self._ffmpeg_installed:\n            return\n        self._ffmpeg_installed = False\n        path = self._conf.ffmpeg_path\n        if hasattr(self._conf, \"data_dir\"):\n            path += os.path.pathsep + os.path.join(getattr(self._conf, \"data_dir\"), \"ffmpeg\", \"bin\")\n        path += os.path.pathsep + self._env_copy.get(\"PATH\", \"\")\n        self._which_ffmpeg, self._which_ffprobe = await asyncio.get_running_loop().run_in_executor(\n            None, self._which_ffmpeg_and_ffmprobe, path\n        )\n        if not self._which_ffmpeg:\n            log.warning(\"Unable to locate ffmpeg executable. Path: %s\", path)\n            raise FileNotFoundError(f\"Unable to locate ffmpeg executable. Path: {path}\")\n        if not self._which_ffprobe:\n            log.warning(\"Unable to locate ffprobe executable. Path: %s\", path)\n            raise FileNotFoundError(f\"Unable to locate ffprobe executable. Path: {path}\")\n        if os.path.dirname(self._which_ffmpeg) != os.path.dirname(self._which_ffprobe):\n            log.warning(\"ffmpeg and ffprobe are in different folders!\")\n\n        await self._verify_executables()\n        self._ffmpeg_installed = True\n\n    async def status(self, reset=False, recheck=False):\n        if reset:\n            self._available_encoders = \"\"\n            self._ffmpeg_installed = None\n        if self._checked_ffmpeg and not recheck:\n            pass\n        elif self._ffmpeg_installed is None:\n            try:\n                await self._verify_ffmpeg_installed()\n            except FileNotFoundError:\n                pass\n            self._checked_ffmpeg = True\n        return {\n            \"available\": self._ffmpeg_installed,\n            \"which\": self._which_ffmpeg,\n            \"analyze_audio_volume\": int(self._conf.volume_analysis_time) > 0\n        }\n\n    @staticmethod\n    def _verify_container(scan_data: json):\n        container = scan_data[\"format\"][\"format_name\"]\n        log.debug(\"   Detected container is %s\", container)\n        splits = container.split(\",\")\n        if not {\"webm\", \"mp4\", \"3gp\", \"ogg\"}.intersection(splits):\n            return \"Container format is not in the approved list of WebM, MP4. \" \\\n                   f\"Actual: {container} [{scan_data['format']['format_long_name']}]\"\n\n        if \"matroska\" in splits:\n            for stream in scan_data[\"streams\"]:\n                if stream[\"codec_type\"] == \"video\":\n                    codec = stream[\"codec_name\"]\n                    if not {\"vp8\", \"vp9\", \"av1\"}.intersection(codec.split(\",\")):\n                        return \"WebM format requires VP8/9 or AV1 video. \" \\\n                               f\"Actual: {codec} [{stream['codec_long_name']}]\"\n                elif stream[\"codec_type\"] == \"audio\":\n                    codec = stream[\"codec_name\"]\n                    if not {\"vorbis\", \"opus\"}.intersection(codec.split(\",\")):\n                        return \"WebM format requires Vorbis or Opus audio. \" \\\n                               f\"Actual: {codec} [{stream['codec_long_name']}]\"\n\n        return \"\"\n\n    @staticmethod\n    def _verify_video_encoding(scan_data: json):\n        for stream in scan_data[\"streams\"]:\n            if stream[\"codec_type\"] != \"video\":\n                continue\n            codec = stream[\"codec_name\"]\n            log.debug(\"   Detected video codec is %s, format is %s\", codec, stream[\"pix_fmt\"])\n            if not {\"h264\", \"vp8\", \"vp9\", \"av1\", \"theora\"}.intersection(codec.split(\",\")):\n                return \"Video codec is not in the approved list of H264, VP8, VP9, AV1, Theora. \" \\\n                       f\"Actual: {codec} [{stream['codec_long_name']}]\"\n\n            if \"h264\" in codec.split(\",\") and stream[\"pix_fmt\"] != \"yuv420p\":\n                return \"Video codec is H264, but its pixel format does not match the approved yuv420p. \" \\\n                       f\"Actual: {stream['pix_fmt']}\"\n\n        return \"\"\n\n    def _verify_bitrate(self, scan_data: json, file_path):\n        bit_rate_max = float(self._conf.video_bitrate_maximum)\n        if bit_rate_max <= 0:\n            return \"\"\n\n        if \"bit_rate\" in scan_data[\"format\"]:\n            bit_rate = float(scan_data[\"format\"][\"bit_rate\"])\n        else:\n            bit_rate = os.stat(file_path).st_size / float(scan_data[\"format\"][\"duration\"])\n        log.debug(\"   Detected bitrate is %s Mbps. Allowed max: %s Mbps\",\n                  str(bit_rate / 1000000.0), str(bit_rate_max / 1000000.0))\n\n        if bit_rate > bit_rate_max:\n            return \"The bit rate is above the configured maximum. Actual: \" \\\n                   f\"{bit_rate / 1000000.0} Mbps; Allowed max: {bit_rate_max / 1000000.0} Mbps\"\n\n        return \"\"\n\n    async def _verify_fast_start(self, scan_data: json, video_file):\n        container = scan_data[\"format\"][\"format_name\"]\n        if {\"webm\", \"ogg\"}.intersection(container.split(\",\")):\n            return \"\"\n\n        result, _ = await self._execute_ffprobe(f'-v debug \"{video_file}\"')\n        match = re.search(r\"Before avformat_find_stream_info.+?\\s+seeks:(\\d+)\\s+\", result)\n        if match and int(match.group(1)) != 0:\n            return \"Video stream descriptors are not at the start of the file (the faststart flag was not used).\"\n        return \"\"\n\n    @staticmethod\n    def _verify_audio_encoding(scan_data: json):\n        for stream in scan_data[\"streams\"]:\n            if stream[\"codec_type\"] != \"audio\":\n                continue\n            codec = stream[\"codec_name\"]\n            log.debug(\"   Detected audio codec is %s\", codec)\n            if not {\"aac\", \"mp3\", \"flac\", \"vorbis\", \"opus\"}.intersection(codec.split(\",\")):\n                return \"Audio codec is not in the approved list of AAC, FLAC, MP3, Vorbis, and Opus. \" \\\n                       f\"Actual: {codec} [{stream['codec_long_name']}]\"\n            if int(stream['sample_rate']) > 48000:\n                return \"Sample rate out of range\"\n\n        return \"\"\n\n    async def _verify_audio_volume(self, seconds, video_file):\n        try:\n            validate_volume = int(seconds) > 0\n        except ValueError:\n            validate_volume = False\n\n        if not validate_volume:\n            return \"\"\n\n        result, _ = await self._execute_ffmpeg(f'-i \"{video_file}\" -t {seconds} '\n                                               f'-af volumedetect -vn -sn -dn -f null \"{os.devnull}\"')\n        try:\n            mean_volume = float(re.search(r\"mean_volume:\\s+([-+]?\\d*\\.\\d+|\\d+)\", result).group(1))\n            max_volume = float(re.search(r\"max_volume:\\s+([-+]?\\d*\\.\\d+|\\d+)\", result).group(1))\n        except Exception as e:\n            log.debug(\"   Failure in volume analysis. Message: %s\", str(e))\n            return \"\"\n\n        if max_volume < -5.0 and mean_volume < -22.0:\n            return \"Audio is at least five dB lower than prime. \" \\\n                   f\"Actual max: {max_volume}, mean: {mean_volume}\"\n\n        log.debug(\"   Detected audio volume has mean, max of %f, %f dB\", mean_volume, max_volume)\n\n        return \"\"\n\n    @staticmethod\n    def _compute_crf(scan_data):\n        height = 240.0\n        for stream in scan_data[\"streams\"]:\n            if stream[\"codec_type\"] == \"video\":\n                height = max(height, float(stream[\"height\"]))\n\n        # https://developers.google.com/media/vp9/settings/vod/\n        return int(-0.011 * height + 40)\n\n    def _get_video_scaler(self):\n        return self._conf.video_scaler\n\n    async def _get_video_encoder(self, scan_data):\n        # use what the user said if it's there:\n        # if it's not there, use h264 if we can because it's way faster than the others\n        # if we don't have h264 use vp9; it's fairly compatible even though it's slow\n\n        if not self._available_encoders:\n            self._available_encoders, _ = await self._execute_ffmpeg(\"-encoders -v quiet\")\n\n        encoder = self._conf.video_encoder.split(\" \", 1)[0]\n        if re.search(fr\"^\\s*V..... {encoder} \", self._available_encoders, re.MULTILINE):\n            return self._conf.video_encoder\n\n        if re.search(r\"^\\s*V..... libx264 \", self._available_encoders, re.MULTILINE):\n            if encoder:\n                log.warning(\"   Using libx264 since the requested encoder was unavailable. Requested: %s\", encoder)\n            return 'libx264 -crf 19 -vf \"format=yuv420p\"'\n\n        if not encoder:\n            encoder = \"libx264\"\n\n        if re.search(r\"^\\s*V..... libvpx-vp9 \", self._available_encoders, re.MULTILINE):\n            log.warning(\"   Using libvpx-vp9 since the requested encoder was unavailable. Requested: %s\", encoder)\n            crf = self._compute_crf(scan_data)\n            return f\"libvpx-vp9 -crf {crf} -b:v 0\"\n\n        if re.search(r\"^\\s*V..... libtheora\", self._available_encoders, re.MULTILINE):\n            log.warning(\"   Using libtheora since the requested encoder was unavailable. Requested: %s\", encoder)\n            return \"libtheora -q:v 7\"\n\n        raise Exception(f\"The video encoder is not available. Requested: {encoder}\")\n\n    async def _get_audio_encoder(self, extension):\n        # if the video encoding is theora or av1/vp8/vp9 use opus (or fallback to vorbis)\n        # or we don't have a video encoding but we have an ogg or webm container use opus\n        # if we need to use opus/vorbis see if the conf file has it else use our own params\n        # else use the user-set value if it exists\n        # else use aac\n\n        wants_opus = extension != \"mp4\"\n        if not self._available_encoders:\n            self._available_encoders, _ = await self._execute_ffmpeg(\"-encoders -v quiet\")\n\n        encoder = self._conf.audio_encoder.split(\" \", 1)[0]\n        if wants_opus and 'opus' in encoder:\n            return self._conf.audio_encoder\n\n        if wants_opus and re.search(r\"^\\s*A..... libopus \", self._available_encoders, re.MULTILINE):\n            return \"libopus -b:a 160k\"\n\n        if wants_opus and 'vorbis' in encoder:\n            return self._conf.audio_encoder\n\n        if wants_opus and re.search(r\"^\\s*A..... libvorbis \", self._available_encoders, re.MULTILINE):\n            return \"libvorbis -q:a 6\"\n\n        if re.search(fr\"^\\s*A..... {encoder} \", self._available_encoders, re.MULTILINE):\n            return self._conf.audio_encoder\n\n        if re.search(r\"^\\s*A..... aac \", self._available_encoders, re.MULTILINE):\n            return \"aac -b:a 192k\"\n\n        raise Exception(f\"The audio encoder is not available. Requested: {encoder or 'aac'}\")\n\n    @staticmethod\n    def _get_best_container_extension(scan_data, video_encoder):\n        # the container is chosen by the video format\n        # if we are theora-encoded, we want ogg\n        # if we are vp8/vp9/av1 we want webm\n        # use mp4 for anything else\n\n        if video_encoder:  # not re-encoding video\n            if \"theora\" in video_encoder:\n                return \"ogv\"\n            if re.search(r\"vp[89x]|av1\", video_encoder.split(\" \", 1)[0]):\n                return \"webm\"\n            return \"mp4\"\n\n        for stream in scan_data[\"streams\"]:\n            if stream[\"codec_type\"] != \"video\":\n                continue\n            codec = stream[\"codec_name\"].split(\",\")\n            if \"theora\" in codec:\n                return \"ogv\"\n            if {\"vp8\", \"vp9\", \"av1\"}.intersection(codec):\n                return \"webm\"\n\n        return \"mp4\"\n\n    async def _get_scan_data(self, validate, file_path):\n        arguments = f'-v quiet -print_format json -show_format -show_streams \"{file_path}\"'\n        result, _ = await self._execute_ffprobe(arguments)\n        try:\n            scan_data = json.loads(result)\n        except Exception as e:\n            log.debug(\"Failure in JSON parsing ffprobe results. Message: %s\", str(e))\n            raise ValueError(f'Absent or unreadable video file: {file_path}')\n\n        if \"format\" not in scan_data or \"duration\" not in scan_data[\"format\"]:\n            log.debug(\"Format data is missing from ffprobe results for: %s\", file_path)\n            raise ValueError(f'Media file does not appear to contain video content: {file_path}')\n\n        if float(scan_data[\"format\"][\"duration\"]) < 0.1:\n            log.debug(\"Media file appears to be an image: %s\", file_path)\n            raise ValueError(f'Assuming image file at: {file_path}')\n\n        return scan_data\n\n    @staticmethod\n    def _build_spec(scan_data):\n        assert scan_data\n\n        duration = ceil(float(scan_data[\"format\"][\"duration\"]))  # existence verified when scan_data made\n        width = -1\n        height = -1\n        for stream in scan_data[\"streams\"]:\n            if stream[\"codec_type\"] != \"video\":\n                continue\n            width = max(width, int(stream[\"width\"]))\n            height = max(height, int(stream[\"height\"]))\n\n        log.debug(\"   Detected duration: %d sec. with resolution: %d x %d\", duration, width, height)\n\n        spec = {\"duration\": duration}\n        if height >= 0:\n            spec[\"height\"] = height\n        if width >= 0:\n            spec[\"width\"] = width\n        return spec\n\n    async def verify_or_repair(self, validate, repair, file_path, ignore_non_video=False):\n        if not validate and not repair:\n            return file_path, {}\n\n        if ignore_non_video and not file_path:\n            return file_path, {}\n\n        await self._verify_ffmpeg_installed()\n        try:\n            scan_data = await self._get_scan_data(validate, file_path)\n        except ValueError:\n            if ignore_non_video:\n                return file_path, {}\n            raise\n\n        fast_start_msg = await self._verify_fast_start(scan_data, file_path)\n        log.debug(\"Analyzing %s:\", file_path)\n        spec = self._build_spec(scan_data)\n        log.debug(\"   Detected faststart is %s\", \"false\" if fast_start_msg else \"true\")\n        container_msg = self._verify_container(scan_data)\n        bitrate_msg = self._verify_bitrate(scan_data, file_path)\n        video_msg = self._verify_video_encoding(scan_data)\n        audio_msg = self._verify_audio_encoding(scan_data)\n        volume_msg = await self._verify_audio_volume(self._conf.volume_analysis_time, file_path)\n        messages = [container_msg, bitrate_msg, fast_start_msg, video_msg, audio_msg, volume_msg]\n\n        if not any(messages):\n            return file_path, spec\n\n        if not repair:\n            errors = [\"Streamability verification failed:\"]\n            errors.extend(filter(None, messages))\n            raise Exception(\"\\n   \".join(errors))\n\n        # the plan for transcoding:\n        # we have to re-encode the video if it is in a nonstandard format\n        # we also re-encode if we are h264 but not yuv420p (both errors caught in video_msg)\n        # we also re-encode if our bitrate or sample rate is too high\n\n        try:\n            transcode_command = [f'-i \"{file_path}\" -y -c:s copy -c:d copy -c:v']\n\n            video_encoder = \"\"\n            if video_msg or bitrate_msg:\n                video_encoder = await self._get_video_encoder(scan_data)\n                transcode_command.append(video_encoder)\n                # could do the scaling only if bitrate_msg, but if we're going to the effort to re-encode anyway...\n                transcode_command.append(self._get_video_scaler())\n            else:\n                transcode_command.append(\"copy\")\n\n            transcode_command.append(\"-movflags +faststart -c:a\")\n            extension = self._get_best_container_extension(scan_data, video_encoder)\n\n            if audio_msg or volume_msg:\n                audio_encoder = await self._get_audio_encoder(extension)\n                transcode_command.append(audio_encoder)\n                if volume_msg and self._conf.volume_filter:\n                    transcode_command.append(self._conf.volume_filter)\n                if audio_msg == \"Sample rate out of range\":\n                    transcode_command.append(\" -ar 48000 \")\n            else:\n                transcode_command.append(\"copy\")\n\n            # TODO: put it in a temp folder and delete it after we upload?\n            path = pathlib.Path(file_path)\n            output = path.parent / f\"{path.stem}_fixed.{extension}\"\n            transcode_command.append(f'\"{output}\"')\n\n            ffmpeg_command = \" \".join(transcode_command)\n            log.info(\"Proceeding on transcode via: ffmpeg %s\", ffmpeg_command)\n            result, code = await self._execute_ffmpeg(ffmpeg_command)\n            if code != 0:\n                raise Exception(f\"Failure to complete the transcode command. Output: {result}\")\n        except Exception as e:\n            if validate:\n                raise\n            log.info(\"Unable to transcode %s . Message: %s\", file_path, str(e))\n            # TODO: delete partial output file here if it exists?\n            return file_path, spec\n\n        return str(output), spec\n"
  },
  {
    "path": "lbry/prometheus.py",
    "content": "import time\nimport logging\nimport asyncio\nimport asyncio.tasks\nfrom aiohttp import web\nfrom prometheus_client import generate_latest as prom_generate_latest\nfrom prometheus_client import Counter, Histogram, Gauge\n\n\nPROBES_IN_FLIGHT = Counter(\"probes_in_flight\", \"Number of loop probes in flight\", namespace='asyncio')\nPROBES_FINISHED = Counter(\"probes_finished\", \"Number of finished loop probes\", namespace='asyncio')\nPROBE_TIMES = Histogram(\"probe_times\", \"Loop probe times\", namespace='asyncio')\nTASK_COUNT = Gauge(\"running_tasks\", \"Number of running tasks\", namespace='asyncio')\n\n\ndef get_loop_metrics(delay=1):\n    loop = asyncio.get_event_loop()\n\n    def callback(started):\n        PROBE_TIMES.observe(time.perf_counter() - started - delay)\n        PROBES_FINISHED.inc()\n\n    async def monitor_loop_responsiveness():\n        while True:\n            now = time.perf_counter()\n            loop.call_later(delay, callback, now)\n            PROBES_IN_FLIGHT.inc()\n            TASK_COUNT.set(len(asyncio.tasks._all_tasks))\n            await asyncio.sleep(delay)\n\n    return loop.create_task(monitor_loop_responsiveness())\n\n\nclass PrometheusServer:\n    def __init__(self, logger=None):\n        self.runner = None\n        self.logger = logger or logging.getLogger(__name__)\n        self._monitor_loop_task = None\n\n    async def start(self, interface: str, port: int):\n        self.logger.info(\"start prometheus metrics\")\n        prom_app = web.Application()\n        prom_app.router.add_get('/metrics', self.handle_metrics_get_request)\n        self.runner = web.AppRunner(prom_app)\n        await self.runner.setup()\n\n        metrics_site = web.TCPSite(self.runner, interface, port, shutdown_timeout=.5)\n        await metrics_site.start()\n        self.logger.info(\n            'prometheus metrics server listening on %s:%i', *metrics_site._server.sockets[0].getsockname()[:2]\n        )\n        self._monitor_loop_task = get_loop_metrics()\n\n    async def handle_metrics_get_request(self, request: web.Request):\n        try:\n            return web.Response(\n                text=prom_generate_latest().decode(),\n                content_type='text/plain; version=0.0.4'\n            )\n        except Exception:\n            self.logger.exception('could not generate prometheus data')\n            raise\n\n    async def stop(self):\n        if self._monitor_loop_task and not self._monitor_loop_task.done():\n            self._monitor_loop_task.cancel()\n        self._monitor_loop_task = None\n        await self.runner.cleanup()\n"
  },
  {
    "path": "lbry/schema/Makefile",
    "content": "build:\n\trm types/v2/* -rf\n\ttouch types/v2/__init__.py\n\tcd types/v2/ && protoc --python_out=. -I ../../../../../types/v2/proto/ ../../../../../types/v2/proto/*.proto\n\tcd types/v2/ && cp ../../../../../types/jsonschema/* ./\n\tsed -e 's/^import\\ \\(.*\\)_pb2\\ /from . import\\ \\1_pb2\\ /g' -i types/v2/*.py\n"
  },
  {
    "path": "lbry/schema/README.md",
    "content": "Schema\n=====\n\nThose files are generated from the [types repo](https://github.com/lbryio/types). If you are modifying/adding a new type, make sure it is cloned in the same root folder as the SDK repo, like:\n\n```\nrepos/\n    - lbry-sdk/\n    - types/\n```\n\nThen, [download protoc 3.2.0](https://github.com/protocolbuffers/protobuf/releases/tag/v3.2.0), add it to your PATH. On linux it is:\n\n```bash\ncd ~/.local/bin\nwget https://github.com/protocolbuffers/protobuf/releases/download/v3.2.0/protoc-3.2.0-linux-x86_64.zip\nunzip protoc-3.2.0-linux-x86_64.zip bin/protoc -d..\n```\n\nFinally, `make` should update everything in place.\n\n\n### Why protoc 3.2.0?\nDifferent/newer versions will generate larger diffs and we need to make sure they are good. In theory, we can just update to latest and it will all work, but it is a good practice to check blockchain data and retro compatibility before bumping versions (if you do, please update this section!).\n"
  },
  {
    "path": "lbry/schema/__init__.py",
    "content": "from .claim import Claim"
  },
  {
    "path": "lbry/schema/attrs.py",
    "content": "import json\nimport logging\nimport os.path\nimport hashlib\nfrom typing import Tuple, List\nfrom string import ascii_letters\nfrom decimal import Decimal, ROUND_UP\nfrom binascii import hexlify, unhexlify\nfrom google.protobuf.json_format import MessageToDict\n\nfrom lbry.crypto.base58 import Base58\nfrom lbry.constants import COIN\nfrom lbry.error import MissingPublishedFileError, EmptyPublishedFileError\n\nfrom lbry.schema.mime_types import guess_media_type\nfrom lbry.schema.base import Metadata, BaseMessageList\nfrom lbry.schema.tags import clean_tags, normalize_tag\nfrom lbry.schema.types.v2.claim_pb2 import (\n    Fee as FeeMessage,\n    Location as LocationMessage,\n    Language as LanguageMessage\n)\n\n\nlog = logging.getLogger(__name__)\n\n\ndef calculate_sha384_file_hash(file_path):\n    sha384 = hashlib.sha384()\n    with open(file_path, 'rb') as f:\n        for chunk in iter(lambda: f.read(128 * sha384.block_size), b''):\n            sha384.update(chunk)\n    return sha384.digest()\n\n\ndef country_int_to_str(country: int) -> str:\n    r = LocationMessage.Country.Name(country)\n    return r[1:] if r.startswith('R') else r\n\n\ndef country_str_to_int(country: str) -> int:\n    if len(country) == 3:\n        country = 'R' + country\n    return LocationMessage.Country.Value(country)\n\n\nclass Dimmensional(Metadata):\n\n    __slots__ = ()\n\n    @property\n    def width(self) -> int:\n        return self.message.width\n\n    @width.setter\n    def width(self, width: int):\n        self.message.width = width\n\n    @property\n    def height(self) -> int:\n        return self.message.height\n\n    @height.setter\n    def height(self, height: int):\n        self.message.height = height\n\n    @property\n    def dimensions(self) -> Tuple[int, int]:\n        return self.width, self.height\n\n    @dimensions.setter\n    def dimensions(self, dimensions: Tuple[int, int]):\n        self.message.width, self.message.height = dimensions\n\n    def _extract(self, file_metadata, field):\n        try:\n            setattr(self, field, file_metadata.getValues(field)[0])\n        except:\n            log.exception(f'Could not extract {field} from file metadata.')\n\n    def update(self, file_metadata=None, height=None, width=None):\n        if height is not None:\n            self.height = height\n        elif file_metadata:\n            self._extract(file_metadata, 'height')\n\n        if width is not None:\n            self.width = width\n        elif file_metadata:\n            self._extract(file_metadata, 'width')\n\n\nclass Playable(Metadata):\n\n    __slots__ = ()\n\n    @property\n    def duration(self) -> int:\n        return self.message.duration\n\n    @duration.setter\n    def duration(self, duration: int):\n        self.message.duration = duration\n\n    def update(self, file_metadata=None, duration=None):\n        if duration is not None:\n            self.duration = duration\n        elif file_metadata:\n            try:\n                self.duration = file_metadata.getValues('duration')[0].seconds\n            except:\n                log.exception('Could not extract duration from file metadata.')\n\n\nclass Image(Dimmensional):\n\n    __slots__ = ()\n\n\nclass Audio(Playable):\n\n    __slots__ = ()\n\n\nclass Video(Dimmensional, Playable):\n\n    __slots__ = ()\n\n    def update(self, file_metadata=None, height=None, width=None, duration=None):\n        Dimmensional.update(self, file_metadata, height, width)\n        Playable.update(self, file_metadata, duration)\n\n\nclass Source(Metadata):\n\n    __slots__ = ()\n\n    def update(self, file_path=None):\n        if file_path is not None:\n            self.name = os.path.basename(file_path)\n            self.media_type, stream_type = guess_media_type(file_path)\n            if not os.path.isfile(file_path):\n                raise MissingPublishedFileError(file_path)\n            self.size = os.path.getsize(file_path)\n            if self.size == 0:\n                raise EmptyPublishedFileError(file_path)\n            self.file_hash_bytes = calculate_sha384_file_hash(file_path)\n            return stream_type\n\n    @property\n    def name(self) -> str:\n        return self.message.name\n\n    @name.setter\n    def name(self, name: str):\n        self.message.name = name\n\n    @property\n    def size(self) -> int:\n        return self.message.size\n\n    @size.setter\n    def size(self, size: int):\n        self.message.size = size\n\n    @property\n    def media_type(self) -> str:\n        return self.message.media_type\n\n    @media_type.setter\n    def media_type(self, media_type: str):\n        self.message.media_type = media_type\n\n    @property\n    def file_hash(self) -> str:\n        return hexlify(self.message.hash).decode()\n\n    @file_hash.setter\n    def file_hash(self, file_hash: str):\n        self.message.hash = unhexlify(file_hash.encode())\n\n    @property\n    def file_hash_bytes(self) -> bytes:\n        return self.message.hash\n\n    @file_hash_bytes.setter\n    def file_hash_bytes(self, file_hash_bytes: bytes):\n        self.message.hash = file_hash_bytes\n\n    @property\n    def sd_hash(self) -> str:\n        return hexlify(self.message.sd_hash).decode()\n\n    @sd_hash.setter\n    def sd_hash(self, sd_hash: str):\n        self.message.sd_hash = unhexlify(sd_hash.encode())\n\n    @property\n    def sd_hash_bytes(self) -> bytes:\n        return self.message.sd_hash\n\n    @sd_hash_bytes.setter\n    def sd_hash_bytes(self, sd_hash: bytes):\n        self.message.sd_hash = sd_hash\n\n    @property\n    def bt_infohash(self) -> str:\n        return hexlify(self.message.bt_infohash).decode()\n\n    @bt_infohash.setter\n    def bt_infohash(self, bt_infohash: str):\n        self.message.bt_infohash = unhexlify(bt_infohash.encode())\n\n    @property\n    def bt_infohash_bytes(self) -> bytes:\n        return self.message.bt_infohash.decode()\n\n    @bt_infohash_bytes.setter\n    def bt_infohash_bytes(self, bt_infohash: bytes):\n        self.message.bt_infohash = bt_infohash\n\n    @property\n    def url(self) -> str:\n        return self.message.url\n\n    @url.setter\n    def url(self, url: str):\n        self.message.url = url\n\n\nclass Fee(Metadata):\n\n    __slots__ = ()\n\n    def update(self, address: str = None, currency: str = None, amount=None):\n        if amount:\n            currency = (currency or self.currency or '').lower()\n            if not currency:\n                raise Exception('In order to set a fee amount, please specify a fee currency.')\n            if currency not in ('lbc', 'btc', 'usd'):\n                raise Exception(f'Missing or unknown currency provided: {currency}')\n            setattr(self, currency, Decimal(amount))\n        elif currency:\n            raise Exception('In order to set a fee currency, please specify a fee amount.')\n        if address:\n            if not self.currency:\n                raise Exception('In order to set a fee address, please specify a fee amount and currency.')\n            self.address = address\n\n    @property\n    def currency(self) -> str:\n        if self.message.currency:\n            return FeeMessage.Currency.Name(self.message.currency)\n\n    @property\n    def address(self) -> str:\n        if self.address_bytes:\n            return Base58.encode(self.address_bytes)\n\n    @address.setter\n    def address(self, address: str):\n        self.address_bytes = Base58.decode(address)\n\n    @property\n    def address_bytes(self) -> bytes:\n        return self.message.address\n\n    @address_bytes.setter\n    def address_bytes(self, address: bytes):\n        self.message.address = address\n\n    @property\n    def amount(self) -> Decimal:\n        if self.currency == 'LBC':\n            return self.lbc\n        if self.currency == 'BTC':\n            return self.btc\n        if self.currency == 'USD':\n            return self.usd\n\n    DEWIES = Decimal(COIN)\n\n    @property\n    def lbc(self) -> Decimal:\n        if self.message.currency != FeeMessage.LBC:\n            raise ValueError('LBC can only be returned for LBC fees.')\n        return Decimal(self.message.amount / self.DEWIES)\n\n    @lbc.setter\n    def lbc(self, amount: Decimal):\n        self.dewies = int(amount * self.DEWIES)\n\n    @property\n    def dewies(self) -> int:\n        if self.message.currency != FeeMessage.LBC:\n            raise ValueError('Dewies can only be returned for LBC fees.')\n        return self.message.amount\n\n    @dewies.setter\n    def dewies(self, amount: int):\n        self.message.amount = amount\n        self.message.currency = FeeMessage.LBC\n\n    SATOSHIES = Decimal(COIN)\n\n    @property\n    def btc(self) -> Decimal:\n        if self.message.currency != FeeMessage.BTC:\n            raise ValueError('BTC can only be returned for BTC fees.')\n        return Decimal(self.message.amount / self.SATOSHIES)\n\n    @btc.setter\n    def btc(self, amount: Decimal):\n        self.satoshis = int(amount * self.SATOSHIES)\n\n    @property\n    def satoshis(self) -> int:\n        if self.message.currency != FeeMessage.BTC:\n            raise ValueError('Satoshies can only be returned for BTC fees.')\n        return self.message.amount\n\n    @satoshis.setter\n    def satoshis(self, amount: int):\n        self.message.amount = amount\n        self.message.currency = FeeMessage.BTC\n\n    PENNIES = Decimal('100.0')\n    PENNY = Decimal('0.01')\n\n    @property\n    def usd(self) -> Decimal:\n        if self.message.currency != FeeMessage.USD:\n            raise ValueError('USD can only be returned for USD fees.')\n        return Decimal(self.message.amount / self.PENNIES)\n\n    @usd.setter\n    def usd(self, amount: Decimal):\n        self.pennies = int(amount.quantize(self.PENNY, ROUND_UP) * self.PENNIES)\n\n    @property\n    def pennies(self) -> int:\n        if self.message.currency != FeeMessage.USD:\n            raise ValueError('Pennies can only be returned for USD fees.')\n        return self.message.amount\n\n    @pennies.setter\n    def pennies(self, amount: int):\n        self.message.amount = amount\n        self.message.currency = FeeMessage.USD\n\n\nclass ClaimReference(Metadata):\n\n    __slots__ = ()\n\n    @property\n    def claim_id(self) -> str:\n        return hexlify(self.claim_hash[::-1]).decode()\n\n    @claim_id.setter\n    def claim_id(self, claim_id: str):\n        self.claim_hash = unhexlify(claim_id)[::-1]\n\n    @property\n    def claim_hash(self) -> bytes:\n        return self.message.claim_hash\n\n    @claim_hash.setter\n    def claim_hash(self, claim_hash: bytes):\n        self.message.claim_hash = claim_hash\n\n\nclass ClaimList(BaseMessageList[ClaimReference]):\n\n    __slots__ = ()\n    item_class = ClaimReference\n\n    @property\n    def _message(self):\n        return self.message.claim_references\n\n    def append(self, value):\n        self.add().claim_id = value\n\n    @property\n    def ids(self) -> List[str]:\n        return [c.claim_id for c in self]\n\n\nclass Language(Metadata):\n\n    __slots__ = ()\n\n    @property\n    def langtag(self) -> str:\n        langtag = []\n        if self.language:\n            langtag.append(self.language)\n        if self.script:\n            langtag.append(self.script)\n        if self.region:\n            langtag.append(self.region)\n        return '-'.join(langtag)\n\n    @langtag.setter\n    def langtag(self, langtag: str):\n        parts = langtag.split('-')\n        self.language = parts.pop(0)\n        if parts and len(parts[0]) == 4:\n            self.script = parts.pop(0)\n        if parts and len(parts[0]) == 2 and parts[0].isalpha():\n            self.region = parts.pop(0)\n        if parts and len(parts[0]) == 3 and parts[0].isdigit():\n            self.region = parts.pop(0)\n        assert not parts, f\"Failed to parse language tag: {langtag}\"\n\n    @property\n    def language(self) -> str:\n        if self.message.language:\n            return LanguageMessage.Language.Name(self.message.language)\n\n    @language.setter\n    def language(self, language: str):\n        self.message.language = LanguageMessage.Language.Value(language)\n\n    @property\n    def script(self) -> str:\n        if self.message.script:\n            return LanguageMessage.Script.Name(self.message.script)\n\n    @script.setter\n    def script(self, script: str):\n        self.message.script = LanguageMessage.Script.Value(script)\n\n    @property\n    def region(self) -> str:\n        if self.message.region:\n            return country_int_to_str(self.message.region)\n\n    @region.setter\n    def region(self, region: str):\n        self.message.region = country_str_to_int(region)\n\n\nclass LanguageList(BaseMessageList[Language]):\n    __slots__ = ()\n    item_class = Language\n\n    def append(self, value: str):\n        self.add().langtag = value\n\n\nclass Location(Metadata):\n\n    __slots__ = ()\n\n    def from_value(self, value):\n        if isinstance(value, str) and value.startswith('{'):\n            value = json.loads(value)\n\n        if isinstance(value, dict):\n            for key, val in value.items():\n                setattr(self, key, val)\n\n        elif isinstance(value, str):\n            parts = value.split(':')\n            if len(parts) > 2 or (parts[0] and parts[0][0] in ascii_letters):\n                country = parts and parts.pop(0)\n                if country:\n                    self.country = country\n                state = parts and parts.pop(0)\n                if state:\n                    self.state = state\n                city = parts and parts.pop(0)\n                if city:\n                    self.city = city\n                code = parts and parts.pop(0)\n                if code:\n                    self.code = code\n            latitude = parts and parts.pop(0)\n            if latitude:\n                self.latitude = latitude\n            longitude = parts and parts.pop(0)\n            if longitude:\n                self.longitude = longitude\n\n        else:\n            raise ValueError(f'Could not parse country value: {value}')\n\n    def to_dict(self):\n        d = MessageToDict(self.message)\n        if self.message.longitude:\n            d['longitude'] = self.longitude\n        if self.message.latitude:\n            d['latitude'] = self.latitude\n        return d\n\n    @property\n    def country(self) -> str:\n        if self.message.country:\n            return LocationMessage.Country.Name(self.message.country)\n\n    @country.setter\n    def country(self, country: str):\n        self.message.country = LocationMessage.Country.Value(country)\n\n    @property\n    def state(self) -> str:\n        return self.message.state\n\n    @state.setter\n    def state(self, state: str):\n        self.message.state = state\n\n    @property\n    def city(self) -> str:\n        return self.message.city\n\n    @city.setter\n    def city(self, city: str):\n        self.message.city = city\n\n    @property\n    def code(self) -> str:\n        return self.message.code\n\n    @code.setter\n    def code(self, code: str):\n        self.message.code = code\n\n    GPS_PRECISION = Decimal('10000000')\n\n    @property\n    def latitude(self) -> str:\n        if self.message.latitude:\n            return str(Decimal(self.message.latitude) / self.GPS_PRECISION)\n\n    @latitude.setter\n    def latitude(self, latitude: str):\n        latitude = Decimal(latitude)\n        assert -90 <= latitude <= 90, \"Latitude must be between -90 and 90 degrees.\"\n        self.message.latitude = int(latitude * self.GPS_PRECISION)\n\n    @property\n    def longitude(self) -> str:\n        if self.message.longitude:\n            return str(Decimal(self.message.longitude) / self.GPS_PRECISION)\n\n    @longitude.setter\n    def longitude(self, longitude: str):\n        longitude = Decimal(longitude)\n        assert -180 <= longitude <= 180, \"Longitude must be between -180 and 180 degrees.\"\n        self.message.longitude = int(longitude * self.GPS_PRECISION)\n\n\nclass LocationList(BaseMessageList[Location]):\n    __slots__ = ()\n    item_class = Location\n\n    def append(self, value):\n        self.add().from_value(value)\n\n\nclass TagList(BaseMessageList[str]):\n    __slots__ = ()\n    item_class = str\n\n    def append(self, tag: str):\n        tag = normalize_tag(tag)\n        if tag and tag not in self.message:\n            self.message.append(tag)\n"
  },
  {
    "path": "lbry/schema/base.py",
    "content": "from binascii import hexlify, unhexlify\nfrom typing import List, Iterator, TypeVar, Generic\n\nfrom google.protobuf.message import DecodeError\nfrom google.protobuf.json_format import MessageToDict\n\n\nclass Signable:\n\n    __slots__ = (\n        'message', 'version', 'signature',\n        'signature_type', 'unsigned_payload', 'signing_channel_hash'\n    )\n\n    message_class = None\n\n    def __init__(self, message=None):\n        self.message = message or self.message_class()\n        self.version = 2\n        self.signature = None\n        self.signature_type = 'SECP256k1'\n        self.unsigned_payload = None\n        self.signing_channel_hash = None\n\n    def clear_signature(self):\n        self.signature = None\n        self.unsigned_payload = None\n        self.signing_channel_hash = None\n\n    @property\n    def signing_channel_id(self):\n        return hexlify(self.signing_channel_hash[::-1]).decode() if self.signing_channel_hash else None\n\n    @signing_channel_id.setter\n    def signing_channel_id(self, channel_id: str):\n        self.signing_channel_hash = unhexlify(channel_id)[::-1]\n\n    @property\n    def is_signed(self):\n        return self.signature is not None\n\n    def to_dict(self):\n        return MessageToDict(self.message)\n\n    def to_message_bytes(self) -> bytes:\n        return self.message.SerializeToString()\n\n    def to_bytes(self) -> bytes:\n        pieces = bytearray()\n        if self.is_signed:\n            pieces.append(1)\n            pieces.extend(self.signing_channel_hash)\n            pieces.extend(self.signature)\n        else:\n            pieces.append(0)\n        pieces.extend(self.to_message_bytes())\n        return bytes(pieces)\n\n    @classmethod\n    def from_bytes(cls, data: bytes):\n        signable = cls()\n        if data[0] == 0:\n            signable.message.ParseFromString(data[1:])\n        elif data[0] == 1:\n            signable.signing_channel_hash = data[1:21]\n            signable.signature = data[21:85]\n            signable.message.ParseFromString(data[85:])\n        else:\n            raise DecodeError('Could not determine message format version.')\n        return signable\n\n    def __len__(self):\n        return len(self.to_bytes())\n\n    def __bytes__(self):\n        return self.to_bytes()\n\n\nclass Metadata:\n\n    __slots__ = 'message',\n\n    def __init__(self, message):\n        self.message = message\n\n\nI = TypeVar('I')\n\n\nclass BaseMessageList(Metadata, Generic[I]):\n\n    __slots__ = ()\n\n    item_class = None\n\n    @property\n    def _message(self):\n        return self.message\n\n    def add(self) -> I:\n        return self.item_class(self._message.add())\n\n    def extend(self, values: List[str]):\n        for value in values:\n            self.append(value)\n\n    def append(self, value: str):\n        raise NotImplemented\n\n    def __len__(self):\n        return len(self._message)\n\n    def __iter__(self) -> Iterator[I]:\n        for item in self._message:\n            yield self.item_class(item)\n\n    def __getitem__(self, item) -> I:\n        return self.item_class(self._message[item])\n\n    def __delitem__(self, key):\n        del self._message[key]\n\n    def __eq__(self, other) -> bool:\n        return self._message == other\n"
  },
  {
    "path": "lbry/schema/claim.py",
    "content": "import logging\nfrom typing import List\nfrom binascii import hexlify, unhexlify\n\nfrom asn1crypto.keys import PublicKeyInfo\nfrom coincurve import PublicKey as cPublicKey\n\nfrom google.protobuf.json_format import MessageToDict\nfrom google.protobuf.message import DecodeError\nfrom hachoir.core.log import log as hachoir_log\nfrom hachoir.parser import createParser as binary_file_parser\nfrom hachoir.metadata import extractMetadata as binary_file_metadata\n\nfrom lbry.schema import compat\nfrom lbry.schema.base import Signable\nfrom lbry.schema.mime_types import guess_media_type, guess_stream_type\nfrom lbry.schema.attrs import (\n    Source, Playable, Dimmensional, Fee, Image, Video, Audio,\n    LanguageList, LocationList, ClaimList, ClaimReference, TagList\n)\nfrom lbry.schema.types.v2.claim_pb2 import Claim as ClaimMessage\nfrom lbry.error import InputValueIsNoneError\n\n\nhachoir_log.use_print = False\nlog = logging.getLogger(__name__)\n\n\nclass Claim(Signable):\n\n    STREAM = 'stream'\n    CHANNEL = 'channel'\n    COLLECTION = 'collection'\n    REPOST = 'repost'\n\n    __slots__ = ()\n\n    message_class = ClaimMessage\n\n    @property\n    def claim_type(self) -> str:\n        return self.message.WhichOneof('type')\n\n    def get_message(self, type_name):\n        message = getattr(self.message, type_name)\n        if self.claim_type is None:\n            message.SetInParent()\n        if self.claim_type != type_name:\n            raise ValueError(f'Claim is not a {type_name}.')\n        return message\n\n    @property\n    def is_stream(self):\n        return self.claim_type == self.STREAM\n\n    @property\n    def stream(self) -> 'Stream':\n        return Stream(self)\n\n    @property\n    def is_channel(self):\n        return self.claim_type == self.CHANNEL\n\n    @property\n    def channel(self) -> 'Channel':\n        return Channel(self)\n\n    @property\n    def is_repost(self):\n        return self.claim_type == self.REPOST\n\n    @property\n    def repost(self) -> 'Repost':\n        return Repost(self)\n\n    @property\n    def is_collection(self):\n        return self.claim_type == self.COLLECTION\n\n    @property\n    def collection(self) -> 'Collection':\n        return Collection(self)\n\n    @classmethod\n    def from_bytes(cls, data: bytes) -> 'Claim':\n        try:\n            return super().from_bytes(data)\n        except DecodeError:\n            claim = cls()\n            if data[0] == ord('{'):\n                claim.version = 0\n                compat.from_old_json_schema(claim, data)\n            elif data[0] not in (0, 1):\n                claim.version = 1\n                compat.from_types_v1(claim, data)\n            else:\n                raise\n            return claim\n\n\nclass BaseClaim:\n\n    __slots__ = 'claim', 'message'\n\n    claim_type = None\n    object_fields = 'thumbnail',\n    repeat_fields = 'tags', 'languages', 'locations'\n\n    def __init__(self, claim: Claim = None):\n        self.claim = claim or Claim()\n        self.message = self.claim.get_message(self.claim_type)\n\n    def to_dict(self):\n        claim = MessageToDict(self.claim.message, preserving_proto_field_name=True)\n        claim.update(claim.pop(self.claim_type))\n        if 'languages' in claim:\n            claim['languages'] = self.langtags\n        if 'locations' in claim:\n            claim['locations'] = [l.to_dict() for l in self.locations]\n        return claim\n\n    def none_check(self, kwargs):\n        for key, value in kwargs.items():\n            if value is None:\n                raise InputValueIsNoneError(key)\n\n    def update(self, **kwargs):\n        self.none_check(kwargs)\n\n        for key in list(kwargs):\n            for field in self.object_fields:\n                if key.startswith(f'{field}_'):\n                    attr = getattr(self, field)\n                    setattr(attr, key[len(f'{field}_'):], kwargs.pop(key))\n                    continue\n\n        for l in self.repeat_fields:\n            field = getattr(self, l)\n            if kwargs.pop(f'clear_{l}', False):\n                del field[:]\n            items = kwargs.pop(l, None)\n            if items is not None:\n                if isinstance(items, str):\n                    field.append(items)\n                elif isinstance(items, list):\n                    field.extend(items)\n                else:\n                    raise ValueError(f\"Unknown {l} value: {items}\")\n\n        for key, value in kwargs.items():\n            setattr(self, key, value)\n\n    @property\n    def title(self) -> str:\n        return self.claim.message.title\n\n    @title.setter\n    def title(self, title: str):\n        self.claim.message.title = title\n\n    @property\n    def description(self) -> str:\n        return self.claim.message.description\n\n    @description.setter\n    def description(self, description: str):\n        self.claim.message.description = description\n\n    @property\n    def thumbnail(self) -> Source:\n        return Source(self.claim.message.thumbnail)\n\n    @property\n    def tags(self) -> List[str]:\n        return TagList(self.claim.message.tags)\n\n    @property\n    def languages(self) -> LanguageList:\n        return LanguageList(self.claim.message.languages)\n\n    @property\n    def langtags(self) -> List[str]:\n        return [l.langtag for l in self.languages]\n\n    @property\n    def locations(self) -> LocationList:\n        return LocationList(self.claim.message.locations)\n\n\nclass Stream(BaseClaim):\n\n    __slots__ = ()\n\n    claim_type = Claim.STREAM\n\n    object_fields = BaseClaim.object_fields + ('source',)\n\n    def to_dict(self):\n        claim = super().to_dict()\n        if 'source' in claim:\n            if 'hash' in claim['source']:\n                claim['source']['hash'] = self.source.file_hash\n            if 'sd_hash' in claim['source']:\n                claim['source']['sd_hash'] = self.source.sd_hash\n            elif 'bt_infohash' in claim['source']:\n                claim['source']['bt_infohash'] = self.source.bt_infohash\n            if 'media_type' in claim['source']:\n                claim['stream_type'] = guess_stream_type(claim['source']['media_type'])\n        fee = claim.get('fee', {})\n        if 'address' in fee:\n            fee['address'] = self.fee.address\n        if 'amount' in fee:\n            fee['amount'] = str(self.fee.amount)\n        return claim\n\n    def update(self, file_path=None, height=None, width=None, duration=None, **kwargs):\n\n        if kwargs.pop('clear_fee', False):\n            self.message.ClearField('fee')\n        else:\n            self.fee.update(\n                kwargs.pop('fee_address', None),\n                kwargs.pop('fee_currency', None),\n                kwargs.pop('fee_amount', None)\n            )\n\n        self.none_check(kwargs)\n\n        if 'sd_hash' in kwargs:\n            self.source.sd_hash = kwargs.pop('sd_hash')\n        elif 'bt_infohash' in kwargs:\n            self.source.bt_infohash = kwargs.pop('bt_infohash')\n        if 'file_name' in kwargs:\n            self.source.name = kwargs.pop('file_name')\n        if 'file_hash' in kwargs:\n            self.source.file_hash = kwargs.pop('file_hash')\n\n        stream_type = None\n        if file_path is not None:\n            stream_type = self.source.update(file_path=file_path)\n        elif self.source.name:\n            self.source.media_type, stream_type = guess_media_type(self.source.name)\n        elif self.source.media_type:\n            stream_type = guess_stream_type(self.source.media_type)\n\n        if 'file_size' in kwargs:\n            self.source.size = kwargs.pop('file_size')\n\n        if self.stream_type is not None and self.stream_type != stream_type:\n            self.message.ClearField(self.stream_type)\n\n        if stream_type in ('image', 'video', 'audio'):\n            media = getattr(self, stream_type)\n            media_args = {'file_metadata': None}\n            if file_path is not None and not all((duration, width, height)):\n                try:\n                    media_args['file_metadata'] = binary_file_metadata(binary_file_parser(file_path))\n                except:\n                    log.exception('Could not read file metadata.')\n            if isinstance(media, Playable):\n                media_args['duration'] = duration\n            if isinstance(media, Dimmensional):\n                media_args['height'] = height\n                media_args['width'] = width\n            media.update(**media_args)\n\n        super().update(**kwargs)\n\n    @property\n    def author(self) -> str:\n        return self.message.author\n\n    @author.setter\n    def author(self, author: str):\n        self.message.author = author\n\n    @property\n    def license(self) -> str:\n        return self.message.license\n\n    @license.setter\n    def license(self, license: str):\n        self.message.license = license\n\n    @property\n    def license_url(self) -> str:\n        return self.message.license_url\n\n    @license_url.setter\n    def license_url(self, license_url: str):\n        self.message.license_url = license_url\n\n    @property\n    def release_time(self) -> int:\n        return self.message.release_time\n\n    @release_time.setter\n    def release_time(self, release_time: int):\n        self.message.release_time = release_time\n\n    @property\n    def fee(self) -> Fee:\n        return Fee(self.message.fee)\n\n    @property\n    def has_fee(self) -> bool:\n        return self.message.HasField('fee')\n\n    @property\n    def has_source(self) -> bool:\n        return self.message.HasField('source')\n\n    @property\n    def source(self) -> Source:\n        return Source(self.message.source)\n\n    @property\n    def stream_type(self) -> str:\n        return self.message.WhichOneof('type')\n\n    @property\n    def image(self) -> Image:\n        return Image(self.message.image)\n\n    @property\n    def video(self) -> Video:\n        return Video(self.message.video)\n\n    @property\n    def audio(self) -> Audio:\n        return Audio(self.message.audio)\n\n\nclass Channel(BaseClaim):\n\n    __slots__ = ()\n\n    claim_type = Claim.CHANNEL\n\n    object_fields = BaseClaim.object_fields + ('cover',)\n    repeat_fields = BaseClaim.repeat_fields + ('featured',)\n\n    def to_dict(self):\n        claim = super().to_dict()\n        claim['public_key'] = self.public_key\n        if 'featured' in claim:\n            claim['featured'] = self.featured.ids\n        return claim\n\n    @property\n    def public_key(self) -> str:\n        return hexlify(self.public_key_bytes).decode()\n\n    @public_key.setter\n    def public_key(self, sd_public_key: str):\n        self.message.public_key = unhexlify(sd_public_key.encode())\n\n    @property\n    def public_key_bytes(self) -> bytes:\n        if len(self.message.public_key) == 33:\n            return self.message.public_key\n        public_key_info = PublicKeyInfo.load(self.message.public_key)\n        public_key = cPublicKey(public_key_info.native['public_key'])\n        return public_key.format(compressed=True)\n\n    @public_key_bytes.setter\n    def public_key_bytes(self, public_key: bytes):\n        self.message.public_key = public_key\n\n    @property\n    def email(self) -> str:\n        return self.message.email\n\n    @email.setter\n    def email(self, email: str):\n        self.message.email = email\n\n    @property\n    def website_url(self) -> str:\n        return self.message.website_url\n\n    @website_url.setter\n    def website_url(self, website_url: str):\n        self.message.website_url = website_url\n\n    @property\n    def cover(self) -> Source:\n        return Source(self.message.cover)\n\n    @property\n    def featured(self) -> ClaimList:\n        return ClaimList(self.message.featured)\n\n\nclass Repost(BaseClaim):\n\n    __slots__ = ()\n\n    claim_type = Claim.REPOST\n\n    def to_dict(self):\n        claim = super().to_dict()\n        if claim.pop('claim_hash', None):\n            claim['claim_id'] = self.reference.claim_id\n        return claim\n\n    @property\n    def reference(self) -> ClaimReference:\n        return ClaimReference(self.message)\n\n\nclass Collection(BaseClaim):\n\n    __slots__ = ()\n\n    claim_type = Claim.COLLECTION\n\n    repeat_fields = BaseClaim.repeat_fields + ('claims',)\n\n    def to_dict(self):\n        claim = super().to_dict()\n        if claim.pop('claim_references', None):\n            claim['claims'] = self.claims.ids\n        return claim\n\n    @property\n    def claims(self) -> ClaimList:\n        return ClaimList(self.message)\n"
  },
  {
    "path": "lbry/schema/compat.py",
    "content": "import json\nfrom decimal import Decimal\n\nfrom google.protobuf.message import DecodeError\n\nfrom lbry.schema.types.v1.legacy_claim_pb2 import Claim as OldClaimMessage\nfrom lbry.schema.types.v1.certificate_pb2 import KeyType\nfrom lbry.schema.types.v1.fee_pb2 import Fee as FeeMessage\n\n\ndef from_old_json_schema(claim, payload: bytes):\n    try:\n        value = json.loads(payload)\n    except:\n        raise DecodeError('Could not parse JSON.')\n    stream = claim.stream\n    stream.source.sd_hash = value['sources']['lbry_sd_hash']\n    stream.source.media_type = (\n            value.get('content_type', value.get('content-type')) or\n            'application/octet-stream'\n    )\n    stream.title = value.get('title', '')\n    stream.description = value.get('description', '')\n    if value.get('thumbnail', ''):\n        stream.thumbnail.url = value.get('thumbnail', '')\n    stream.author = value.get('author', '')\n    stream.license = value.get('license', '')\n    stream.license_url = value.get('license_url', '')\n    language = value.get('language', '')\n    if language:\n        if language.lower() == 'english':\n            language = 'en'\n        try:\n            stream.languages.append(language)\n        except:\n            pass\n    if value.get('nsfw', False):\n        stream.tags.append('mature')\n    if \"fee\" in value and isinstance(value['fee'], dict):\n        fee = value[\"fee\"]\n        currency = list(fee.keys())[0]\n        if currency == 'LBC':\n            stream.fee.lbc = Decimal(fee[currency]['amount'])\n        elif currency == 'USD':\n            stream.fee.usd = Decimal(fee[currency]['amount'])\n        elif currency == 'BTC':\n            stream.fee.btc = Decimal(fee[currency]['amount'])\n        else:\n            raise DecodeError(f'Unknown currency: {currency}')\n        stream.fee.address = fee[currency]['address']\n    return claim\n\n\ndef from_types_v1(claim, payload: bytes):\n    old = OldClaimMessage()\n    old.ParseFromString(payload)\n    if old.claimType == 2:\n        channel = claim.channel\n        channel.public_key_bytes = old.certificate.publicKey\n    else:\n        stream = claim.stream\n        stream.title = old.stream.metadata.title\n        stream.description = old.stream.metadata.description\n        stream.author = old.stream.metadata.author\n        stream.license = old.stream.metadata.license\n        stream.license_url = old.stream.metadata.licenseUrl\n        stream.thumbnail.url = old.stream.metadata.thumbnail\n        if old.stream.metadata.HasField('language'):\n            stream.languages.add().message.language = old.stream.metadata.language\n        stream.source.media_type = old.stream.source.contentType\n        stream.source.sd_hash_bytes = old.stream.source.source\n        if old.stream.metadata.nsfw:\n            stream.tags.append('mature')\n        if old.stream.metadata.HasField('fee'):\n            fee = old.stream.metadata.fee\n            stream.fee.address_bytes = fee.address\n            currency = FeeMessage.Currency.Name(fee.currency)\n            if currency == 'LBC':\n                stream.fee.lbc = Decimal(fee.amount)\n            elif currency == 'USD':\n                stream.fee.usd = Decimal(fee.amount)\n            elif currency == 'BTC':\n                stream.fee.btc = Decimal(fee.amount)\n            else:\n                raise DecodeError(f'Unsupported currency: {currency}')\n        if old.HasField('publisherSignature'):\n            sig = old.publisherSignature\n            claim.signature = sig.signature\n            claim.signature_type = KeyType.Name(sig.signatureType)\n            claim.signing_channel_hash = sig.certificateId[::-1]\n            old.ClearField(\"publisherSignature\")\n            claim.unsigned_payload = old.SerializeToString()\n    return claim\n"
  },
  {
    "path": "lbry/schema/mime_types.py",
    "content": "import os\nimport filetype\nimport logging\n\ntypes_map = {\n    # http://www.iana.org/assignments/media-types\n    # Type mapping for automated metadata extraction (video, audio, image, document, binary, model)\n    '.a': ('application/octet-stream', 'binary'),\n    '.ai': ('application/postscript', 'image'),\n    '.aif': ('audio/x-aiff', 'audio'),\n    '.aifc': ('audio/x-aiff', 'audio'),\n    '.aiff': ('audio/x-aiff', 'audio'),\n    '.au': ('audio/basic', 'audio'),\n    '.avi': ('video/x-msvideo', 'video'),\n    '.bat': ('text/plain', 'document'),\n    '.bcpio': ('application/x-bcpio', 'binary'),\n    '.bin': ('application/octet-stream', 'binary'),\n    '.bmp': ('image/bmp', 'image'),\n    '.c': ('text/plain', 'document'),\n    '.cdf': ('application/x-netcdf', 'binary'),\n    '.cpio': ('application/x-cpio', 'binary'),\n    '.csh': ('application/x-csh', 'binary'),\n    '.css': ('text/css', 'document'),\n    '.csv': ('text/csv', 'document'),\n    '.dll': ('application/octet-stream', 'binary'),\n    '.doc': ('application/msword', 'document'),\n    '.dot': ('application/msword', 'document'),\n    '.dvi': ('application/x-dvi', 'binary'),\n    '.eml': ('message/rfc822', 'document'),\n    '.eps': ('application/postscript', 'document'),\n    '.epub': ('application/epub+zip', 'document'),\n    '.etx': ('text/x-setext', 'document'),\n    '.exe': ('application/octet-stream', 'binary'),\n    '.gif': ('image/gif', 'image'),\n    '.gtar': ('application/x-gtar', 'binary'),\n    '.h': ('text/plain', 'document'),\n    '.hdf': ('application/x-hdf', 'binary'),\n    '.htm': ('text/html', 'document'),\n    '.html': ('text/html', 'document'),\n    '.ico': ('image/vnd.microsoft.icon', 'image'),\n    '.ief': ('image/ief', 'image'),\n    '.iges': ('model/iges', 'model'),\n    '.jpe': ('image/jpeg', 'image'),\n    '.jpeg': ('image/jpeg', 'image'),\n    '.jpg': ('image/jpeg', 'image'),\n    '.js': ('application/javascript', 'document'),\n    '.json': ('application/json', 'document'),\n    '.ksh': ('text/plain', 'document'),\n    '.latex': ('application/x-latex', 'binary'),\n    '.m1v': ('video/mpeg', 'video'),\n    '.m3u': ('application/x-mpegurl', 'audio'),\n    '.m3u8': ('application/x-mpegurl', 'video'),\n    '.man': ('application/x-troff-man', 'document'),\n    '.markdown': ('text/markdown', 'document'),\n    '.md': ('text/markdown', 'document'),\n    '.me': ('application/x-troff-me', 'binary'),\n    '.mht': ('message/rfc822', 'document'),\n    '.mhtml': ('message/rfc822', 'document'),\n    '.mif': ('application/x-mif', 'binary'),\n    '.mov': ('video/quicktime', 'video'),\n    '.movie': ('video/x-sgi-movie', 'video'),\n    '.mp2': ('audio/mpeg', 'audio'),\n    '.mp3': ('audio/mpeg', 'audio'),\n    '.mp4': ('video/mp4', 'video'),\n    '.mpa': ('video/mpeg', 'video'),\n    '.mpd': ('application/dash+xml', 'video'),\n    '.mpe': ('video/mpeg', 'video'),\n    '.mpeg': ('video/mpeg', 'video'),\n    '.mpg': ('video/mpeg', 'video'),\n    '.ms': ('application/x-troff-ms', 'binary'),\n    '.m4s': ('video/iso.segment', 'binary'),\n    '.nc': ('application/x-netcdf', 'binary'),\n    '.nws': ('message/rfc822', 'document'),\n    '.o': ('application/octet-stream', 'binary'),\n    '.obj': ('application/octet-stream', 'model'),\n    '.oda': ('application/oda', 'binary'),\n    '.p12': ('application/x-pkcs12', 'binary'),\n    '.p7c': ('application/pkcs7-mime', 'binary'),\n    '.pbm': ('image/x-portable-bitmap', 'image'),\n    '.pdf': ('application/pdf', 'document'),\n    '.pfx': ('application/x-pkcs12', 'binary'),\n    '.pgm': ('image/x-portable-graymap', 'image'),\n    '.pl': ('text/plain', 'document'),\n    '.png': ('image/png', 'image'),\n    '.pnm': ('image/x-portable-anymap', 'image'),\n    '.pot': ('application/vnd.ms-powerpoint', 'document'),\n    '.ppa': ('application/vnd.ms-powerpoint', 'document'),\n    '.ppm': ('image/x-portable-pixmap', 'image'),\n    '.pps': ('application/vnd.ms-powerpoint', 'document'),\n    '.ppt': ('application/vnd.ms-powerpoint', 'document'),\n    '.ps': ('application/postscript', 'document'),\n    '.pwz': ('application/vnd.ms-powerpoint', 'document'),\n    '.py': ('text/x-python', 'document'),\n    '.pyc': ('application/x-python-code', 'binary'),\n    '.pyo': ('application/x-python-code', 'binary'),\n    '.qt': ('video/quicktime', 'video'),\n    '.ra': ('audio/x-pn-realaudio', 'audio'),\n    '.ram': ('application/x-pn-realaudio', 'audio'),\n    '.ras': ('image/x-cmu-raster', 'image'),\n    '.rdf': ('application/xml', 'binary'),\n    '.rgb': ('image/x-rgb', 'image'),\n    '.roff': ('application/x-troff', 'binary'),\n    '.rtx': ('text/richtext', 'document'),\n    '.sgm': ('text/x-sgml', 'document'),\n    '.sgml': ('text/x-sgml', 'document'),\n    '.sh': ('application/x-sh', 'document'),\n    '.shar': ('application/x-shar', 'binary'),\n    '.snd': ('audio/basic', 'audio'),\n    '.so': ('application/octet-stream', 'binary'),\n    '.src': ('application/x-wais-source', 'binary'),\n    '.stl': ('model/stl', 'model'),\n    '.sv4cpio': ('application/x-sv4cpio', 'binary'),\n    '.sv4crc': ('application/x-sv4crc', 'binary'),\n    '.svg': ('image/svg+xml', 'image'),\n    '.swf': ('application/x-shockwave-flash', 'binary'),\n    '.t': ('application/x-troff', 'binary'),\n    '.tar': ('application/x-tar', 'binary'),\n    '.tcl': ('application/x-tcl', 'binary'),\n    '.tex': ('application/x-tex', 'binary'),\n    '.texi': ('application/x-texinfo', 'binary'),\n    '.texinfo': ('application/x-texinfo', 'binary'),\n    '.tif': ('image/tiff', 'image'),\n    '.tiff': ('image/tiff', 'image'),\n    '.tr': ('application/x-troff', 'binary'),\n    '.ts': ('video/mp2t', 'video'),\n    '.tsv': ('text/tab-separated-values', 'document'),\n    '.txt': ('text/plain', 'document'),\n    '.ustar': ('application/x-ustar', 'binary'),\n    '.vcf': ('text/x-vcard', 'document'),\n    '.vtt': ('text/vtt', 'document'),\n    '.wav': ('audio/x-wav', 'audio'),\n    '.webm': ('video/webm', 'video'),\n    '.wiz': ('application/msword', 'document'),\n    '.wsdl': ('application/xml', 'document'),\n    '.xbm': ('image/x-xbitmap', 'image'),\n    '.xlb': ('application/vnd.ms-excel', 'document'),\n    '.xls': ('application/vnd.ms-excel', 'document'),\n    '.xml': ('text/xml', 'document'),\n    '.xpdl': ('application/xml', 'document'),\n    '.xpm': ('image/x-xpixmap', 'image'),\n    '.xsl': ('application/xml', 'document'),\n    '.xwd': ('image/x-xwindowdump', 'image'),\n    '.zip': ('application/zip', 'binary'),\n\n    # These are non-standard types, commonly found in the wild.\n    '.cbr': ('application/vnd.comicbook-rar', 'document'),\n    '.cbz': ('application/vnd.comicbook+zip', 'document'),\n    '.flac': ('audio/flac', 'audio'),\n    '.lbry': ('application/x-ext-lbry', 'document'),\n    '.m4a': ('audio/mp4', 'audio'),\n    '.m4v': ('video/m4v', 'video'),\n    '.mid': ('audio/midi', 'audio'),\n    '.midi': ('audio/midi', 'audio'),\n    '.mkv': ('video/x-matroska', 'video'),\n    '.mobi': ('application/x-mobipocket-ebook', 'document'),\n    '.oga': ('audio/ogg', 'audio'),\n    '.ogv': ('video/ogg', 'video'),\n    '.ogg': ('video/ogg', 'video'),\n    '.pct': ('image/pict', 'image'),\n    '.pic': ('image/pict', 'image'),\n    '.pict': ('image/pict', 'image'),\n    '.prc': ('application/x-mobipocket-ebook', 'document'),\n    '.rtf': ('application/rtf', 'document'),\n    '.xul': ('text/xul', 'document'),\n    \n    # microsoft is special and has its own 'standard'\n    # https://docs.microsoft.com/en-us/windows/desktop/wmp/file-name-extensions\n    '.wmv': ('video/x-ms-wmv', 'video')\n}\n\n# maps detected extensions to the possible analogs\n# i.e. .cbz file is actually a .zip\nsynonyms_map = {\n    '.zip': ['.cbz'],\n    '.rar': ['.cbr'],\n    '.ar': ['.a']\n}\n\nlog = logging.getLogger(__name__)\n\n\ndef guess_media_type(path):\n    _, ext = os.path.splitext(path)\n    extension = ext.strip().lower()\n\n    try:\n        kind = filetype.guess(path)\n        if kind:\n            real_extension = f\".{kind.extension}\"\n\n            if extension != real_extension:\n                if extension:\n                    log.warning(f\"file extension does not match it's contents: {path}, identified as {real_extension}\")\n                else:\n                    log.debug(f\"file {path} does not have extension, identified by it's contents as {real_extension}\")\n\n                if extension not in synonyms_map.get(real_extension, []):\n                    extension = real_extension\n\n    except OSError as error:\n        pass\n\n    if extension[1:]:\n        if extension in types_map:\n            return types_map[extension]\n        return f'application/x-ext-{extension[1:]}', 'binary'\n    return 'application/octet-stream', 'binary'\n\n\ndef guess_stream_type(media_type):\n    for media, stream in types_map.values():\n        if media == media_type:\n            return stream\n    return 'binary'\n"
  },
  {
    "path": "lbry/schema/purchase.py",
    "content": "from google.protobuf.message import DecodeError\nfrom google.protobuf.json_format import MessageToDict\nfrom lbry.schema.types.v2.purchase_pb2 import Purchase as PurchaseMessage\nfrom .attrs import ClaimReference\n\n\nclass Purchase(ClaimReference):\n\n    START_BYTE = ord('P')\n\n    __slots__ = ()\n\n    def __init__(self, claim_id=None):\n        super().__init__(PurchaseMessage())\n        if claim_id is not None:\n            self.claim_id = claim_id\n\n    def to_dict(self):\n        return MessageToDict(self.message)\n\n    def to_message_bytes(self) -> bytes:\n        return self.message.SerializeToString()\n\n    def to_bytes(self) -> bytes:\n        pieces = bytearray()\n        pieces.append(self.START_BYTE)\n        pieces.extend(self.to_message_bytes())\n        return bytes(pieces)\n\n    @classmethod\n    def has_start_byte(cls, data: bytes):\n        return data and data[0] == cls.START_BYTE\n\n    @classmethod\n    def from_bytes(cls, data: bytes):\n        purchase = cls()\n        if purchase.has_start_byte(data):\n            purchase.message.ParseFromString(data[1:])\n        else:\n            raise DecodeError('Message does not start with correct byte.')\n        return purchase\n\n    def __len__(self):\n        return len(self.to_bytes())\n\n    def __bytes__(self):\n        return self.to_bytes()\n"
  },
  {
    "path": "lbry/schema/result.py",
    "content": "import base64\nfrom typing import List, Union, Optional, NamedTuple\nfrom binascii import hexlify\nfrom itertools import chain\n\nfrom lbry.error import ResolveCensoredError\nfrom lbry.schema.types.v2.result_pb2 import Outputs as OutputsMessage\nfrom lbry.schema.types.v2.result_pb2 import Error as ErrorMessage\n\nINVALID = ErrorMessage.Code.Name(ErrorMessage.INVALID)\nNOT_FOUND = ErrorMessage.Code.Name(ErrorMessage.NOT_FOUND)\nBLOCKED = ErrorMessage.Code.Name(ErrorMessage.BLOCKED)\n\n\ndef set_reference(reference, claim_hash, rows):\n    if claim_hash:\n        for txo in rows:\n            if claim_hash == txo.claim_hash:\n                reference.tx_hash = txo.tx_hash\n                reference.nout = txo.position\n                reference.height = txo.height\n                return\n\n\nclass ResolveResult(NamedTuple):\n    name: str\n    normalized_name: str\n    claim_hash: bytes\n    tx_num: int\n    position: int\n    tx_hash: bytes\n    height: int\n    amount: int\n    short_url: str\n    is_controlling: bool\n    canonical_url: str\n    creation_height: int\n    activation_height: int\n    expiration_height: int\n    effective_amount: int\n    support_amount: int\n    reposted: int\n    last_takeover_height: Optional[int]\n    claims_in_channel: Optional[int]\n    channel_hash: Optional[bytes]\n    reposted_claim_hash: Optional[bytes]\n    signature_valid: Optional[bool]\n\n\nclass Censor:\n\n    NOT_CENSORED = 0\n    SEARCH = 1\n    RESOLVE = 2\n\n    __slots__ = 'censor_type', 'censored'\n\n    def __init__(self, censor_type):\n        self.censor_type = censor_type\n        self.censored = {}\n\n    def is_censored(self, row):\n        return (row.get('censor_type') or self.NOT_CENSORED) >= self.censor_type\n\n    def apply(self, rows):\n        return [row for row in rows if not self.censor(row)]\n\n    def censor(self, row) -> Optional[bytes]:\n        if self.is_censored(row):\n            censoring_channel_hash = bytes.fromhex(row['censoring_channel_id'])[::-1]\n            self.censored.setdefault(censoring_channel_hash, set())\n            self.censored[censoring_channel_hash].add(row['tx_hash'])\n            return censoring_channel_hash\n        return None\n\n    def to_message(self, outputs: OutputsMessage, extra_txo_rows: dict):\n        for censoring_channel_hash, count in self.censored.items():\n            blocked = outputs.blocked.add()\n            blocked.count = len(count)\n            set_reference(blocked.channel, censoring_channel_hash, extra_txo_rows)\n            outputs.blocked_total += len(count)\n\n\nclass Outputs:\n\n    __slots__ = 'txos', 'extra_txos', 'txs', 'offset', 'total', 'blocked', 'blocked_total'\n\n    def __init__(self, txos: List, extra_txos: List, txs: set,\n                 offset: int, total: int, blocked: List, blocked_total: int):\n        self.txos = txos\n        self.txs = txs\n        self.extra_txos = extra_txos\n        self.offset = offset\n        self.total = total\n        self.blocked = blocked\n        self.blocked_total = blocked_total\n\n    def inflate(self, txs):\n        tx_map = {tx.hash: tx for tx in txs}\n        for txo_message in self.extra_txos:\n            self.message_to_txo(txo_message, tx_map)\n        txos = [self.message_to_txo(txo_message, tx_map) for txo_message in self.txos]\n        return txos, self.inflate_blocked(tx_map)\n\n    def inflate_blocked(self, tx_map):\n        return {\n            \"total\": self.blocked_total,\n            \"channels\": [{\n                'channel': self.message_to_txo(blocked.channel, tx_map),\n                'blocked': blocked.count\n            } for blocked in self.blocked]\n        }\n\n    def message_to_txo(self, txo_message, tx_map):\n        if txo_message.WhichOneof('meta') == 'error':\n            error = {\n                'error': {\n                    'name': txo_message.error.Code.Name(txo_message.error.code),\n                    'text': txo_message.error.text,\n                }\n            }\n            if error['error']['name'] == BLOCKED:\n                error['error']['censor'] = self.message_to_txo(\n                    txo_message.error.blocked.channel, tx_map\n                )\n            return error\n\n        tx = tx_map.get(txo_message.tx_hash)\n        if not tx:\n            return\n        txo = tx.outputs[txo_message.nout]\n        if txo_message.WhichOneof('meta') == 'claim':\n            claim = txo_message.claim\n            txo.meta = {\n                'short_url': f'lbry://{claim.short_url}',\n                'canonical_url': f'lbry://{claim.canonical_url or claim.short_url}',\n                'reposted': claim.reposted,\n                'is_controlling': claim.is_controlling,\n                'take_over_height': claim.take_over_height,\n                'creation_height': claim.creation_height,\n                'activation_height': claim.activation_height,\n                'expiration_height': claim.expiration_height,\n                'effective_amount': claim.effective_amount,\n                'support_amount': claim.support_amount,\n                # 'trending_group': claim.trending_group,\n                # 'trending_mixed': claim.trending_mixed,\n                # 'trending_local': claim.trending_local,\n                # 'trending_global': claim.trending_global,\n            }\n            if claim.HasField('channel'):\n                txo.channel = tx_map[claim.channel.tx_hash].outputs[claim.channel.nout]\n            if claim.HasField('repost'):\n                txo.reposted_claim = tx_map[claim.repost.tx_hash].outputs[claim.repost.nout]\n            try:\n                if txo.claim.is_channel:\n                    txo.meta['claims_in_channel'] = claim.claims_in_channel\n            except:\n                pass\n        return txo\n\n    @classmethod\n    def from_base64(cls, data: str) -> 'Outputs':\n        return cls.from_bytes(base64.b64decode(data))\n\n    @classmethod\n    def from_bytes(cls, data: bytes) -> 'Outputs':\n        outputs = OutputsMessage()\n        outputs.ParseFromString(data)\n        txs = set()\n        for txo_message in chain(outputs.txos, outputs.extra_txos):\n            if txo_message.WhichOneof('meta') == 'error':\n                continue\n            txs.add((hexlify(txo_message.tx_hash[::-1]).decode(), txo_message.height))\n        return cls(\n            outputs.txos, outputs.extra_txos, txs,\n            outputs.offset, outputs.total,\n            outputs.blocked, outputs.blocked_total\n        )\n\n    @classmethod\n    def to_base64(cls, txo_rows, extra_txo_rows, offset=0, total=None, blocked=None) -> str:\n        return base64.b64encode(cls.to_bytes(txo_rows, extra_txo_rows, offset, total, blocked)).decode()\n\n    @classmethod\n    def to_bytes(cls, txo_rows, extra_txo_rows, offset=0, total=None, blocked: Censor = None) -> bytes:\n        page = OutputsMessage()\n        page.offset = offset\n        if total is not None:\n            page.total = total\n        if blocked is not None:\n            blocked.to_message(page, extra_txo_rows)\n        for row in extra_txo_rows:\n            txo_message: 'OutputsMessage' = page.extra_txos.add()\n            if not isinstance(row, Exception):\n                if row.channel_hash:\n                    set_reference(txo_message.claim.channel, row.channel_hash, extra_txo_rows)\n                if row.reposted_claim_hash:\n                    set_reference(txo_message.claim.repost, row.reposted_claim_hash, extra_txo_rows)\n            cls.encode_txo(txo_message, row)\n\n        for row in txo_rows:\n            # cls.row_to_message(row, page.txos.add(), extra_txo_rows)\n            txo_message: 'OutputsMessage' = page.txos.add()\n            cls.encode_txo(txo_message, row)\n            if not isinstance(row, Exception):\n                if row.channel_hash:\n                    set_reference(txo_message.claim.channel, row.channel_hash, extra_txo_rows)\n                if row.reposted_claim_hash:\n                    set_reference(txo_message.claim.repost, row.reposted_claim_hash, extra_txo_rows)\n            elif isinstance(row, ResolveCensoredError):\n                set_reference(txo_message.error.blocked.channel, row.censor_id, extra_txo_rows)\n        return page.SerializeToString()\n\n    @classmethod\n    def encode_txo(cls, txo_message, resolve_result: Union['ResolveResult', Exception]):\n        if isinstance(resolve_result, Exception):\n            txo_message.error.text = resolve_result.args[0]\n            if isinstance(resolve_result, ValueError):\n                txo_message.error.code = ErrorMessage.INVALID\n            elif isinstance(resolve_result, LookupError):\n                txo_message.error.code = ErrorMessage.NOT_FOUND\n            elif isinstance(resolve_result, ResolveCensoredError):\n                txo_message.error.code = ErrorMessage.BLOCKED\n            return\n        txo_message.tx_hash = resolve_result.tx_hash\n        txo_message.nout = resolve_result.position\n        txo_message.height = resolve_result.height\n        txo_message.claim.short_url = resolve_result.short_url\n        txo_message.claim.reposted = resolve_result.reposted\n        txo_message.claim.is_controlling = resolve_result.is_controlling\n        txo_message.claim.creation_height = resolve_result.creation_height\n        txo_message.claim.activation_height = resolve_result.activation_height\n        txo_message.claim.expiration_height = resolve_result.expiration_height\n        txo_message.claim.effective_amount = resolve_result.effective_amount\n        txo_message.claim.support_amount = resolve_result.support_amount\n\n        if resolve_result.canonical_url is not None:\n            txo_message.claim.canonical_url = resolve_result.canonical_url\n        if resolve_result.last_takeover_height is not None:\n            txo_message.claim.take_over_height = resolve_result.last_takeover_height\n        if resolve_result.claims_in_channel is not None:\n            txo_message.claim.claims_in_channel = resolve_result.claims_in_channel\n"
  },
  {
    "path": "lbry/schema/support.py",
    "content": "from lbry.schema.base import Signable\nfrom lbry.schema.types.v2.support_pb2 import Support as SupportMessage\n\n\nclass Support(Signable):\n    __slots__ = ()\n    message_class = SupportMessage\n\n    @property\n    def emoji(self) -> str:\n        return self.message.emoji\n\n    @emoji.setter\n    def emoji(self, emoji: str):\n        self.message.emoji = emoji\n\n    @property\n    def comment(self) -> str:\n        return self.message.comment\n\n    @comment.setter\n    def comment(self, comment: str):\n        self.message.comment = comment\n"
  },
  {
    "path": "lbry/schema/tags.py",
    "content": "from typing import List\nimport re\n\nMULTI_SPACE_RE = re.compile(r\"\\s{2,}\")\nWEIRD_CHARS_RE = re.compile(r\"[#!~]\")\n\n\ndef normalize_tag(tag: str):\n    return MULTI_SPACE_RE.sub(' ', WEIRD_CHARS_RE.sub(' ', tag.lower().replace(\"'\", \"\"))).strip()\n\n\ndef clean_tags(tags: List[str]):\n    return [tag for tag in {normalize_tag(tag) for tag in tags} if tag]\n"
  },
  {
    "path": "lbry/schema/types/__init__.py",
    "content": ""
  },
  {
    "path": "lbry/schema/types/v1/__init__.py",
    "content": ""
  },
  {
    "path": "lbry/schema/types/v1/certificate_pb2.py",
    "content": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: certificate.proto\n\nimport sys\n_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))\nfrom google.protobuf.internal import enum_type_wrapper\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import message as _message\nfrom google.protobuf import reflection as _reflection\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor.FileDescriptor(\n  name='certificate.proto',\n  package='legacy_pb',\n  syntax='proto2',\n  serialized_options=None,\n  serialized_pb=_b('\\n\\x11\\x63\\x65rtificate.proto\\x12\\tlegacy_pb\\\"\\xa2\\x01\\n\\x0b\\x43\\x65rtificate\\x12/\\n\\x07version\\x18\\x01 \\x02(\\x0e\\x32\\x1e.legacy_pb.Certificate.Version\\x12#\\n\\x07keyType\\x18\\x02 \\x02(\\x0e\\x32\\x12.legacy_pb.KeyType\\x12\\x11\\n\\tpublicKey\\x18\\x04 \\x02(\\x0c\\\"*\\n\\x07Version\\x12\\x13\\n\\x0fUNKNOWN_VERSION\\x10\\x00\\x12\\n\\n\\x06_0_0_1\\x10\\x01*Q\\n\\x07KeyType\\x12\\x1b\\n\\x17UNKNOWN_PUBLIC_KEY_TYPE\\x10\\x00\\x12\\x0c\\n\\x08NIST256p\\x10\\x01\\x12\\x0c\\n\\x08NIST384p\\x10\\x02\\x12\\r\\n\\tSECP256k1\\x10\\x03')\n)\n\n_KEYTYPE = _descriptor.EnumDescriptor(\n  name='KeyType',\n  full_name='legacy_pb.KeyType',\n  filename=None,\n  file=DESCRIPTOR,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='UNKNOWN_PUBLIC_KEY_TYPE', index=0, number=0,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='NIST256p', index=1, number=1,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='NIST384p', index=2, number=2,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='SECP256k1', index=3, number=3,\n      serialized_options=None,\n      type=None),\n  ],\n  containing_type=None,\n  serialized_options=None,\n  serialized_start=197,\n  serialized_end=278,\n)\n_sym_db.RegisterEnumDescriptor(_KEYTYPE)\n\nKeyType = enum_type_wrapper.EnumTypeWrapper(_KEYTYPE)\nUNKNOWN_PUBLIC_KEY_TYPE = 0\nNIST256p = 1\nNIST384p = 2\nSECP256k1 = 3\n\n\n_CERTIFICATE_VERSION = _descriptor.EnumDescriptor(\n  name='Version',\n  full_name='legacy_pb.Certificate.Version',\n  filename=None,\n  file=DESCRIPTOR,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='UNKNOWN_VERSION', index=0, number=0,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='_0_0_1', index=1, number=1,\n      serialized_options=None,\n      type=None),\n  ],\n  containing_type=None,\n  serialized_options=None,\n  serialized_start=153,\n  serialized_end=195,\n)\n_sym_db.RegisterEnumDescriptor(_CERTIFICATE_VERSION)\n\n\n_CERTIFICATE = _descriptor.Descriptor(\n  name='Certificate',\n  full_name='legacy_pb.Certificate',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='version', full_name='legacy_pb.Certificate.version', index=0,\n      number=1, type=14, cpp_type=8, label=2,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='keyType', full_name='legacy_pb.Certificate.keyType', index=1,\n      number=2, type=14, cpp_type=8, label=2,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='publicKey', full_name='legacy_pb.Certificate.publicKey', index=2,\n      number=4, type=12, cpp_type=9, label=2,\n      has_default_value=False, default_value=_b(\"\"),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n    _CERTIFICATE_VERSION,\n  ],\n  serialized_options=None,\n  is_extendable=False,\n  syntax='proto2',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=33,\n  serialized_end=195,\n)\n\n_CERTIFICATE.fields_by_name['version'].enum_type = _CERTIFICATE_VERSION\n_CERTIFICATE.fields_by_name['keyType'].enum_type = _KEYTYPE\n_CERTIFICATE_VERSION.containing_type = _CERTIFICATE\nDESCRIPTOR.message_types_by_name['Certificate'] = _CERTIFICATE\nDESCRIPTOR.enum_types_by_name['KeyType'] = _KEYTYPE\n_sym_db.RegisterFileDescriptor(DESCRIPTOR)\n\nCertificate = _reflection.GeneratedProtocolMessageType('Certificate', (_message.Message,), dict(\n  DESCRIPTOR = _CERTIFICATE,\n  __module__ = 'certificate_pb2'\n  # @@protoc_insertion_point(class_scope:legacy_pb.Certificate)\n  ))\n_sym_db.RegisterMessage(Certificate)\n\n\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "lbry/schema/types/v1/fee_pb2.py",
    "content": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: fee.proto\n\nimport sys\n_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import message as _message\nfrom google.protobuf import reflection as _reflection\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor.FileDescriptor(\n  name='fee.proto',\n  package='legacy_pb',\n  syntax='proto2',\n  serialized_options=None,\n  serialized_pb=_b('\\n\\tfee.proto\\x12\\tlegacy_pb\\\"\\xe3\\x01\\n\\x03\\x46\\x65\\x65\\x12\\'\\n\\x07version\\x18\\x01 \\x02(\\x0e\\x32\\x16.legacy_pb.Fee.Version\\x12)\\n\\x08\\x63urrency\\x18\\x02 \\x02(\\x0e\\x32\\x17.legacy_pb.Fee.Currency\\x12\\x0f\\n\\x07\\x61\\x64\\x64ress\\x18\\x03 \\x02(\\x0c\\x12\\x0e\\n\\x06\\x61mount\\x18\\x04 \\x02(\\x02\\\"*\\n\\x07Version\\x12\\x13\\n\\x0fUNKNOWN_VERSION\\x10\\x00\\x12\\n\\n\\x06_0_0_1\\x10\\x01\\\";\\n\\x08\\x43urrency\\x12\\x14\\n\\x10UNKNOWN_CURRENCY\\x10\\x00\\x12\\x07\\n\\x03LBC\\x10\\x01\\x12\\x07\\n\\x03\\x42TC\\x10\\x02\\x12\\x07\\n\\x03USD\\x10\\x03')\n)\n\n\n\n_FEE_VERSION = _descriptor.EnumDescriptor(\n  name='Version',\n  full_name='legacy_pb.Fee.Version',\n  filename=None,\n  file=DESCRIPTOR,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='UNKNOWN_VERSION', index=0, number=0,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='_0_0_1', index=1, number=1,\n      serialized_options=None,\n      type=None),\n  ],\n  containing_type=None,\n  serialized_options=None,\n  serialized_start=149,\n  serialized_end=191,\n)\n_sym_db.RegisterEnumDescriptor(_FEE_VERSION)\n\n_FEE_CURRENCY = _descriptor.EnumDescriptor(\n  name='Currency',\n  full_name='legacy_pb.Fee.Currency',\n  filename=None,\n  file=DESCRIPTOR,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='UNKNOWN_CURRENCY', index=0, number=0,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='LBC', index=1, number=1,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BTC', index=2, number=2,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='USD', index=3, number=3,\n      serialized_options=None,\n      type=None),\n  ],\n  containing_type=None,\n  serialized_options=None,\n  serialized_start=193,\n  serialized_end=252,\n)\n_sym_db.RegisterEnumDescriptor(_FEE_CURRENCY)\n\n\n_FEE = _descriptor.Descriptor(\n  name='Fee',\n  full_name='legacy_pb.Fee',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='version', full_name='legacy_pb.Fee.version', index=0,\n      number=1, type=14, cpp_type=8, label=2,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='currency', full_name='legacy_pb.Fee.currency', index=1,\n      number=2, type=14, cpp_type=8, label=2,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='address', full_name='legacy_pb.Fee.address', index=2,\n      number=3, type=12, cpp_type=9, label=2,\n      has_default_value=False, default_value=_b(\"\"),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='amount', full_name='legacy_pb.Fee.amount', index=3,\n      number=4, type=2, cpp_type=6, label=2,\n      has_default_value=False, default_value=float(0),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n    _FEE_VERSION,\n    _FEE_CURRENCY,\n  ],\n  serialized_options=None,\n  is_extendable=False,\n  syntax='proto2',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=25,\n  serialized_end=252,\n)\n\n_FEE.fields_by_name['version'].enum_type = _FEE_VERSION\n_FEE.fields_by_name['currency'].enum_type = _FEE_CURRENCY\n_FEE_VERSION.containing_type = _FEE\n_FEE_CURRENCY.containing_type = _FEE\nDESCRIPTOR.message_types_by_name['Fee'] = _FEE\n_sym_db.RegisterFileDescriptor(DESCRIPTOR)\n\nFee = _reflection.GeneratedProtocolMessageType('Fee', (_message.Message,), dict(\n  DESCRIPTOR = _FEE,\n  __module__ = 'fee_pb2'\n  # @@protoc_insertion_point(class_scope:legacy_pb.Fee)\n  ))\n_sym_db.RegisterMessage(Fee)\n\n\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "lbry/schema/types/v1/legacy_claim_pb2.py",
    "content": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: legacy_claim.proto\n\nimport sys\n_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import message as _message\nfrom google.protobuf import reflection as _reflection\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\nfrom . import stream_pb2 as stream__pb2\nfrom . import certificate_pb2 as certificate__pb2\nfrom . import signature_pb2 as signature__pb2\n\n\nDESCRIPTOR = _descriptor.FileDescriptor(\n  name='legacy_claim.proto',\n  package='legacy_pb',\n  syntax='proto2',\n  serialized_options=None,\n  serialized_pb=_b('\\n\\x12legacy_claim.proto\\x12\\tlegacy_pb\\x1a\\x0cstream.proto\\x1a\\x11\\x63\\x65rtificate.proto\\x1a\\x0fsignature.proto\\\"\\xd9\\x02\\n\\x05\\x43laim\\x12)\\n\\x07version\\x18\\x01 \\x02(\\x0e\\x32\\x18.legacy_pb.Claim.Version\\x12-\\n\\tclaimType\\x18\\x02 \\x02(\\x0e\\x32\\x1a.legacy_pb.Claim.ClaimType\\x12!\\n\\x06stream\\x18\\x03 \\x01(\\x0b\\x32\\x11.legacy_pb.Stream\\x12+\\n\\x0b\\x63\\x65rtificate\\x18\\x04 \\x01(\\x0b\\x32\\x16.legacy_pb.Certificate\\x12\\x30\\n\\x12publisherSignature\\x18\\x05 \\x01(\\x0b\\x32\\x14.legacy_pb.Signature\\\"*\\n\\x07Version\\x12\\x13\\n\\x0fUNKNOWN_VERSION\\x10\\x00\\x12\\n\\n\\x06_0_0_1\\x10\\x01\\\"H\\n\\tClaimType\\x12\\x16\\n\\x12UNKNOWN_CLAIM_TYPE\\x10\\x00\\x12\\x0e\\n\\nstreamType\\x10\\x01\\x12\\x13\\n\\x0f\\x63\\x65rtificateType\\x10\\x02')\n  ,\n  dependencies=[stream__pb2.DESCRIPTOR,certificate__pb2.DESCRIPTOR,signature__pb2.DESCRIPTOR,])\n\n\n\n_CLAIM_VERSION = _descriptor.EnumDescriptor(\n  name='Version',\n  full_name='legacy_pb.Claim.Version',\n  filename=None,\n  file=DESCRIPTOR,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='UNKNOWN_VERSION', index=0, number=0,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='_0_0_1', index=1, number=1,\n      serialized_options=None,\n      type=None),\n  ],\n  containing_type=None,\n  serialized_options=None,\n  serialized_start=313,\n  serialized_end=355,\n)\n_sym_db.RegisterEnumDescriptor(_CLAIM_VERSION)\n\n_CLAIM_CLAIMTYPE = _descriptor.EnumDescriptor(\n  name='ClaimType',\n  full_name='legacy_pb.Claim.ClaimType',\n  filename=None,\n  file=DESCRIPTOR,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='UNKNOWN_CLAIM_TYPE', index=0, number=0,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='streamType', index=1, number=1,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='certificateType', index=2, number=2,\n      serialized_options=None,\n      type=None),\n  ],\n  containing_type=None,\n  serialized_options=None,\n  serialized_start=357,\n  serialized_end=429,\n)\n_sym_db.RegisterEnumDescriptor(_CLAIM_CLAIMTYPE)\n\n\n_CLAIM = _descriptor.Descriptor(\n  name='Claim',\n  full_name='legacy_pb.Claim',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='version', full_name='legacy_pb.Claim.version', index=0,\n      number=1, type=14, cpp_type=8, label=2,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='claimType', full_name='legacy_pb.Claim.claimType', index=1,\n      number=2, type=14, cpp_type=8, label=2,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='stream', full_name='legacy_pb.Claim.stream', index=2,\n      number=3, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='certificate', full_name='legacy_pb.Claim.certificate', index=3,\n      number=4, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='publisherSignature', full_name='legacy_pb.Claim.publisherSignature', index=4,\n      number=5, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n    _CLAIM_VERSION,\n    _CLAIM_CLAIMTYPE,\n  ],\n  serialized_options=None,\n  is_extendable=False,\n  syntax='proto2',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=84,\n  serialized_end=429,\n)\n\n_CLAIM.fields_by_name['version'].enum_type = _CLAIM_VERSION\n_CLAIM.fields_by_name['claimType'].enum_type = _CLAIM_CLAIMTYPE\n_CLAIM.fields_by_name['stream'].message_type = stream__pb2._STREAM\n_CLAIM.fields_by_name['certificate'].message_type = certificate__pb2._CERTIFICATE\n_CLAIM.fields_by_name['publisherSignature'].message_type = signature__pb2._SIGNATURE\n_CLAIM_VERSION.containing_type = _CLAIM\n_CLAIM_CLAIMTYPE.containing_type = _CLAIM\nDESCRIPTOR.message_types_by_name['Claim'] = _CLAIM\n_sym_db.RegisterFileDescriptor(DESCRIPTOR)\n\nClaim = _reflection.GeneratedProtocolMessageType('Claim', (_message.Message,), dict(\n  DESCRIPTOR = _CLAIM,\n  __module__ = 'legacy_claim_pb2'\n  # @@protoc_insertion_point(class_scope:legacy_pb.Claim)\n  ))\n_sym_db.RegisterMessage(Claim)\n\n\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "lbry/schema/types/v1/metadata_pb2.py",
    "content": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: metadata.proto\n\nimport sys\n_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import message as _message\nfrom google.protobuf import reflection as _reflection\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\nfrom . import fee_pb2 as fee__pb2\n\n\nDESCRIPTOR = _descriptor.FileDescriptor(\n  name='metadata.proto',\n  package='legacy_pb',\n  syntax='proto2',\n  serialized_options=None,\n  serialized_pb=_b('\\n\\x0emetadata.proto\\x12\\tlegacy_pb\\x1a\\tfee.proto\\\"\\xfc\\x0e\\n\\x08Metadata\\x12,\\n\\x07version\\x18\\x01 \\x02(\\x0e\\x32\\x1b.legacy_pb.Metadata.Version\\x12.\\n\\x08language\\x18\\x02 \\x02(\\x0e\\x32\\x1c.legacy_pb.Metadata.Language\\x12\\r\\n\\x05title\\x18\\x03 \\x02(\\t\\x12\\x13\\n\\x0b\\x64\\x65scription\\x18\\x04 \\x02(\\t\\x12\\x0e\\n\\x06\\x61uthor\\x18\\x05 \\x02(\\t\\x12\\x0f\\n\\x07license\\x18\\x06 \\x02(\\t\\x12\\x0c\\n\\x04nsfw\\x18\\x07 \\x02(\\x08\\x12\\x1b\\n\\x03\\x66\\x65\\x65\\x18\\x08 \\x01(\\x0b\\x32\\x0e.legacy_pb.Fee\\x12\\x11\\n\\tthumbnail\\x18\\t \\x01(\\t\\x12\\x0f\\n\\x07preview\\x18\\n \\x01(\\t\\x12\\x12\\n\\nlicenseUrl\\x18\\x0b \\x01(\\t\\\"N\\n\\x07Version\\x12\\x13\\n\\x0fUNKNOWN_VERSION\\x10\\x00\\x12\\n\\n\\x06_0_0_1\\x10\\x01\\x12\\n\\n\\x06_0_0_2\\x10\\x02\\x12\\n\\n\\x06_0_0_3\\x10\\x03\\x12\\n\\n\\x06_0_1_0\\x10\\x04\\\"\\x99\\x0c\\n\\x08Language\\x12\\x14\\n\\x10UNKNOWN_LANGUAGE\\x10\\x00\\x12\\x06\\n\\x02\\x65n\\x10\\x01\\x12\\x06\\n\\x02\\x61\\x61\\x10\\x02\\x12\\x06\\n\\x02\\x61\\x62\\x10\\x03\\x12\\x06\\n\\x02\\x61\\x65\\x10\\x04\\x12\\x06\\n\\x02\\x61\\x66\\x10\\x05\\x12\\x06\\n\\x02\\x61k\\x10\\x06\\x12\\x06\\n\\x02\\x61m\\x10\\x07\\x12\\x06\\n\\x02\\x61n\\x10\\x08\\x12\\x06\\n\\x02\\x61r\\x10\\t\\x12\\x06\\n\\x02\\x61s\\x10\\n\\x12\\x06\\n\\x02\\x61v\\x10\\x0b\\x12\\x06\\n\\x02\\x61y\\x10\\x0c\\x12\\x06\\n\\x02\\x61z\\x10\\r\\x12\\x06\\n\\x02\\x62\\x61\\x10\\x0e\\x12\\x06\\n\\x02\\x62\\x65\\x10\\x0f\\x12\\x06\\n\\x02\\x62g\\x10\\x10\\x12\\x06\\n\\x02\\x62h\\x10\\x11\\x12\\x06\\n\\x02\\x62i\\x10\\x12\\x12\\x06\\n\\x02\\x62m\\x10\\x13\\x12\\x06\\n\\x02\\x62n\\x10\\x14\\x12\\x06\\n\\x02\\x62o\\x10\\x15\\x12\\x06\\n\\x02\\x62r\\x10\\x16\\x12\\x06\\n\\x02\\x62s\\x10\\x17\\x12\\x06\\n\\x02\\x63\\x61\\x10\\x18\\x12\\x06\\n\\x02\\x63\\x65\\x10\\x19\\x12\\x06\\n\\x02\\x63h\\x10\\x1a\\x12\\x06\\n\\x02\\x63o\\x10\\x1b\\x12\\x06\\n\\x02\\x63r\\x10\\x1c\\x12\\x06\\n\\x02\\x63s\\x10\\x1d\\x12\\x06\\n\\x02\\x63u\\x10\\x1e\\x12\\x06\\n\\x02\\x63v\\x10\\x1f\\x12\\x06\\n\\x02\\x63y\\x10 \\x12\\x06\\n\\x02\\x64\\x61\\x10!\\x12\\x06\\n\\x02\\x64\\x65\\x10\\\"\\x12\\x06\\n\\x02\\x64v\\x10#\\x12\\x06\\n\\x02\\x64z\\x10$\\x12\\x06\\n\\x02\\x65\\x65\\x10%\\x12\\x06\\n\\x02\\x65l\\x10&\\x12\\x06\\n\\x02\\x65o\\x10\\'\\x12\\x06\\n\\x02\\x65s\\x10(\\x12\\x06\\n\\x02\\x65t\\x10)\\x12\\x06\\n\\x02\\x65u\\x10*\\x12\\x06\\n\\x02\\x66\\x61\\x10+\\x12\\x06\\n\\x02\\x66\\x66\\x10,\\x12\\x06\\n\\x02\\x66i\\x10-\\x12\\x06\\n\\x02\\x66j\\x10.\\x12\\x06\\n\\x02\\x66o\\x10/\\x12\\x06\\n\\x02\\x66r\\x10\\x30\\x12\\x06\\n\\x02\\x66y\\x10\\x31\\x12\\x06\\n\\x02ga\\x10\\x32\\x12\\x06\\n\\x02gd\\x10\\x33\\x12\\x06\\n\\x02gl\\x10\\x34\\x12\\x06\\n\\x02gn\\x10\\x35\\x12\\x06\\n\\x02gu\\x10\\x36\\x12\\x06\\n\\x02gv\\x10\\x37\\x12\\x06\\n\\x02ha\\x10\\x38\\x12\\x06\\n\\x02he\\x10\\x39\\x12\\x06\\n\\x02hi\\x10:\\x12\\x06\\n\\x02ho\\x10;\\x12\\x06\\n\\x02hr\\x10<\\x12\\x06\\n\\x02ht\\x10=\\x12\\x06\\n\\x02hu\\x10>\\x12\\x06\\n\\x02hy\\x10?\\x12\\x06\\n\\x02hz\\x10@\\x12\\x06\\n\\x02ia\\x10\\x41\\x12\\x06\\n\\x02id\\x10\\x42\\x12\\x06\\n\\x02ie\\x10\\x43\\x12\\x06\\n\\x02ig\\x10\\x44\\x12\\x06\\n\\x02ii\\x10\\x45\\x12\\x06\\n\\x02ik\\x10\\x46\\x12\\x06\\n\\x02io\\x10G\\x12\\x06\\n\\x02is\\x10H\\x12\\x06\\n\\x02it\\x10I\\x12\\x06\\n\\x02iu\\x10J\\x12\\x06\\n\\x02ja\\x10K\\x12\\x06\\n\\x02jv\\x10L\\x12\\x06\\n\\x02ka\\x10M\\x12\\x06\\n\\x02kg\\x10N\\x12\\x06\\n\\x02ki\\x10O\\x12\\x06\\n\\x02kj\\x10P\\x12\\x06\\n\\x02kk\\x10Q\\x12\\x06\\n\\x02kl\\x10R\\x12\\x06\\n\\x02km\\x10S\\x12\\x06\\n\\x02kn\\x10T\\x12\\x06\\n\\x02ko\\x10U\\x12\\x06\\n\\x02kr\\x10V\\x12\\x06\\n\\x02ks\\x10W\\x12\\x06\\n\\x02ku\\x10X\\x12\\x06\\n\\x02kv\\x10Y\\x12\\x06\\n\\x02kw\\x10Z\\x12\\x06\\n\\x02ky\\x10[\\x12\\x06\\n\\x02la\\x10\\\\\\x12\\x06\\n\\x02lb\\x10]\\x12\\x06\\n\\x02lg\\x10^\\x12\\x06\\n\\x02li\\x10_\\x12\\x06\\n\\x02ln\\x10`\\x12\\x06\\n\\x02lo\\x10\\x61\\x12\\x06\\n\\x02lt\\x10\\x62\\x12\\x06\\n\\x02lu\\x10\\x63\\x12\\x06\\n\\x02lv\\x10\\x64\\x12\\x06\\n\\x02mg\\x10\\x65\\x12\\x06\\n\\x02mh\\x10\\x66\\x12\\x06\\n\\x02mi\\x10g\\x12\\x06\\n\\x02mk\\x10h\\x12\\x06\\n\\x02ml\\x10i\\x12\\x06\\n\\x02mn\\x10j\\x12\\x06\\n\\x02mr\\x10k\\x12\\x06\\n\\x02ms\\x10l\\x12\\x06\\n\\x02mt\\x10m\\x12\\x06\\n\\x02my\\x10n\\x12\\x06\\n\\x02na\\x10o\\x12\\x06\\n\\x02nb\\x10p\\x12\\x06\\n\\x02nd\\x10q\\x12\\x06\\n\\x02ne\\x10r\\x12\\x06\\n\\x02ng\\x10s\\x12\\x06\\n\\x02nl\\x10t\\x12\\x06\\n\\x02nn\\x10u\\x12\\x06\\n\\x02no\\x10v\\x12\\x06\\n\\x02nr\\x10w\\x12\\x06\\n\\x02nv\\x10x\\x12\\x06\\n\\x02ny\\x10y\\x12\\x06\\n\\x02oc\\x10z\\x12\\x06\\n\\x02oj\\x10{\\x12\\x06\\n\\x02om\\x10|\\x12\\x06\\n\\x02or\\x10}\\x12\\x06\\n\\x02os\\x10~\\x12\\x06\\n\\x02pa\\x10\\x7f\\x12\\x07\\n\\x02pi\\x10\\x80\\x01\\x12\\x07\\n\\x02pl\\x10\\x81\\x01\\x12\\x07\\n\\x02ps\\x10\\x82\\x01\\x12\\x07\\n\\x02pt\\x10\\x83\\x01\\x12\\x07\\n\\x02qu\\x10\\x84\\x01\\x12\\x07\\n\\x02rm\\x10\\x85\\x01\\x12\\x07\\n\\x02rn\\x10\\x86\\x01\\x12\\x07\\n\\x02ro\\x10\\x87\\x01\\x12\\x07\\n\\x02ru\\x10\\x88\\x01\\x12\\x07\\n\\x02rw\\x10\\x89\\x01\\x12\\x07\\n\\x02sa\\x10\\x8a\\x01\\x12\\x07\\n\\x02sc\\x10\\x8b\\x01\\x12\\x07\\n\\x02sd\\x10\\x8c\\x01\\x12\\x07\\n\\x02se\\x10\\x8d\\x01\\x12\\x07\\n\\x02sg\\x10\\x8e\\x01\\x12\\x07\\n\\x02si\\x10\\x8f\\x01\\x12\\x07\\n\\x02sk\\x10\\x90\\x01\\x12\\x07\\n\\x02sl\\x10\\x91\\x01\\x12\\x07\\n\\x02sm\\x10\\x92\\x01\\x12\\x07\\n\\x02sn\\x10\\x93\\x01\\x12\\x07\\n\\x02so\\x10\\x94\\x01\\x12\\x07\\n\\x02sq\\x10\\x95\\x01\\x12\\x07\\n\\x02sr\\x10\\x96\\x01\\x12\\x07\\n\\x02ss\\x10\\x97\\x01\\x12\\x07\\n\\x02st\\x10\\x98\\x01\\x12\\x07\\n\\x02su\\x10\\x99\\x01\\x12\\x07\\n\\x02sv\\x10\\x9a\\x01\\x12\\x07\\n\\x02sw\\x10\\x9b\\x01\\x12\\x07\\n\\x02ta\\x10\\x9c\\x01\\x12\\x07\\n\\x02te\\x10\\x9d\\x01\\x12\\x07\\n\\x02tg\\x10\\x9e\\x01\\x12\\x07\\n\\x02th\\x10\\x9f\\x01\\x12\\x07\\n\\x02ti\\x10\\xa0\\x01\\x12\\x07\\n\\x02tk\\x10\\xa1\\x01\\x12\\x07\\n\\x02tl\\x10\\xa2\\x01\\x12\\x07\\n\\x02tn\\x10\\xa3\\x01\\x12\\x07\\n\\x02to\\x10\\xa4\\x01\\x12\\x07\\n\\x02tr\\x10\\xa5\\x01\\x12\\x07\\n\\x02ts\\x10\\xa6\\x01\\x12\\x07\\n\\x02tt\\x10\\xa7\\x01\\x12\\x07\\n\\x02tw\\x10\\xa8\\x01\\x12\\x07\\n\\x02ty\\x10\\xa9\\x01\\x12\\x07\\n\\x02ug\\x10\\xaa\\x01\\x12\\x07\\n\\x02uk\\x10\\xab\\x01\\x12\\x07\\n\\x02ur\\x10\\xac\\x01\\x12\\x07\\n\\x02uz\\x10\\xad\\x01\\x12\\x07\\n\\x02ve\\x10\\xae\\x01\\x12\\x07\\n\\x02vi\\x10\\xaf\\x01\\x12\\x07\\n\\x02vo\\x10\\xb0\\x01\\x12\\x07\\n\\x02wa\\x10\\xb1\\x01\\x12\\x07\\n\\x02wo\\x10\\xb2\\x01\\x12\\x07\\n\\x02xh\\x10\\xb3\\x01\\x12\\x07\\n\\x02yi\\x10\\xb4\\x01\\x12\\x07\\n\\x02yo\\x10\\xb5\\x01\\x12\\x07\\n\\x02za\\x10\\xb6\\x01\\x12\\x07\\n\\x02zh\\x10\\xb7\\x01\\x12\\x07\\n\\x02zu\\x10\\xb8\\x01')\n  ,\n  dependencies=[fee__pb2.DESCRIPTOR,])\n\n\n\n_METADATA_VERSION = _descriptor.EnumDescriptor(\n  name='Version',\n  full_name='legacy_pb.Metadata.Version',\n  filename=None,\n  file=DESCRIPTOR,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='UNKNOWN_VERSION', index=0, number=0,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='_0_0_1', index=1, number=1,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='_0_0_2', index=2, number=2,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='_0_0_3', index=3, number=3,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='_0_1_0', index=4, number=4,\n      serialized_options=None,\n      type=None),\n  ],\n  containing_type=None,\n  serialized_options=None,\n  serialized_start=315,\n  serialized_end=393,\n)\n_sym_db.RegisterEnumDescriptor(_METADATA_VERSION)\n\n_METADATA_LANGUAGE = _descriptor.EnumDescriptor(\n  name='Language',\n  full_name='legacy_pb.Metadata.Language',\n  filename=None,\n  file=DESCRIPTOR,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='UNKNOWN_LANGUAGE', index=0, number=0,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='en', index=1, number=1,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='aa', index=2, number=2,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ab', index=3, number=3,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ae', index=4, number=4,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='af', index=5, number=5,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ak', index=6, number=6,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='am', index=7, number=7,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='an', index=8, number=8,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ar', index=9, number=9,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='as', index=10, number=10,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='av', index=11, number=11,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ay', index=12, number=12,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='az', index=13, number=13,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ba', index=14, number=14,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='be', index=15, number=15,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='bg', index=16, number=16,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='bh', index=17, number=17,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='bi', index=18, number=18,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='bm', index=19, number=19,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='bn', index=20, number=20,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='bo', index=21, number=21,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='br', index=22, number=22,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='bs', index=23, number=23,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ca', index=24, number=24,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ce', index=25, number=25,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ch', index=26, number=26,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='co', index=27, number=27,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='cr', index=28, number=28,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='cs', index=29, number=29,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='cu', index=30, number=30,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='cv', index=31, number=31,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='cy', index=32, number=32,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='da', index=33, number=33,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='de', index=34, number=34,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='dv', index=35, number=35,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='dz', index=36, number=36,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ee', index=37, number=37,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='el', index=38, number=38,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='eo', index=39, number=39,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='es', index=40, number=40,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='et', index=41, number=41,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='eu', index=42, number=42,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='fa', index=43, number=43,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ff', index=44, number=44,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='fi', index=45, number=45,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='fj', index=46, number=46,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='fo', index=47, number=47,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='fr', index=48, number=48,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='fy', index=49, number=49,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ga', index=50, number=50,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='gd', index=51, number=51,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='gl', index=52, number=52,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='gn', index=53, number=53,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='gu', index=54, number=54,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='gv', index=55, number=55,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ha', index=56, number=56,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='he', index=57, number=57,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='hi', index=58, number=58,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ho', index=59, number=59,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='hr', index=60, number=60,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ht', index=61, number=61,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='hu', index=62, number=62,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='hy', index=63, number=63,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='hz', index=64, number=64,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ia', index=65, number=65,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='id', index=66, number=66,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ie', index=67, number=67,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ig', index=68, number=68,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ii', index=69, number=69,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ik', index=70, number=70,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='io', index=71, number=71,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='is', index=72, number=72,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='it', index=73, number=73,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='iu', index=74, number=74,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ja', index=75, number=75,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='jv', index=76, number=76,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ka', index=77, number=77,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='kg', index=78, number=78,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ki', index=79, number=79,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='kj', index=80, number=80,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='kk', index=81, number=81,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='kl', index=82, number=82,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='km', index=83, number=83,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='kn', index=84, number=84,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ko', index=85, number=85,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='kr', index=86, number=86,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ks', index=87, number=87,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ku', index=88, number=88,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='kv', index=89, number=89,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='kw', index=90, number=90,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ky', index=91, number=91,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='la', index=92, number=92,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='lb', index=93, number=93,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='lg', index=94, number=94,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='li', index=95, number=95,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ln', index=96, number=96,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='lo', index=97, number=97,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='lt', index=98, number=98,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='lu', index=99, number=99,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='lv', index=100, number=100,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='mg', index=101, number=101,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='mh', index=102, number=102,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='mi', index=103, number=103,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='mk', index=104, number=104,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ml', index=105, number=105,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='mn', index=106, number=106,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='mr', index=107, number=107,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ms', index=108, number=108,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='mt', index=109, number=109,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='my', index=110, number=110,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='na', index=111, number=111,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='nb', index=112, number=112,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='nd', index=113, number=113,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ne', index=114, number=114,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ng', index=115, number=115,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='nl', index=116, number=116,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='nn', index=117, number=117,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='no', index=118, number=118,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='nr', index=119, number=119,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='nv', index=120, number=120,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ny', index=121, number=121,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='oc', index=122, number=122,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='oj', index=123, number=123,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='om', index=124, number=124,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='or', index=125, number=125,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='os', index=126, number=126,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='pa', index=127, number=127,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='pi', index=128, number=128,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='pl', index=129, number=129,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ps', index=130, number=130,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='pt', index=131, number=131,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='qu', index=132, number=132,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='rm', index=133, number=133,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='rn', index=134, number=134,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ro', index=135, number=135,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ru', index=136, number=136,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='rw', index=137, number=137,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sa', index=138, number=138,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sc', index=139, number=139,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sd', index=140, number=140,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='se', index=141, number=141,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sg', index=142, number=142,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='si', index=143, number=143,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sk', index=144, number=144,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sl', index=145, number=145,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sm', index=146, number=146,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sn', index=147, number=147,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='so', index=148, number=148,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sq', index=149, number=149,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sr', index=150, number=150,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ss', index=151, number=151,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='st', index=152, number=152,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='su', index=153, number=153,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sv', index=154, number=154,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sw', index=155, number=155,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ta', index=156, number=156,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='te', index=157, number=157,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='tg', index=158, number=158,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='th', index=159, number=159,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ti', index=160, number=160,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='tk', index=161, number=161,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='tl', index=162, number=162,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='tn', index=163, number=163,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='to', index=164, number=164,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='tr', index=165, number=165,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ts', index=166, number=166,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='tt', index=167, number=167,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='tw', index=168, number=168,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ty', index=169, number=169,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ug', index=170, number=170,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='uk', index=171, number=171,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ur', index=172, number=172,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='uz', index=173, number=173,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ve', index=174, number=174,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='vi', index=175, number=175,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='vo', index=176, number=176,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='wa', index=177, number=177,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='wo', index=178, number=178,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='xh', index=179, number=179,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='yi', index=180, number=180,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='yo', index=181, number=181,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='za', index=182, number=182,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='zh', index=183, number=183,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='zu', index=184, number=184,\n      serialized_options=None,\n      type=None),\n  ],\n  containing_type=None,\n  serialized_options=None,\n  serialized_start=396,\n  serialized_end=1957,\n)\n_sym_db.RegisterEnumDescriptor(_METADATA_LANGUAGE)\n\n\n_METADATA = _descriptor.Descriptor(\n  name='Metadata',\n  full_name='legacy_pb.Metadata',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='version', full_name='legacy_pb.Metadata.version', index=0,\n      number=1, type=14, cpp_type=8, label=2,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='language', full_name='legacy_pb.Metadata.language', index=1,\n      number=2, type=14, cpp_type=8, label=2,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='title', full_name='legacy_pb.Metadata.title', index=2,\n      number=3, type=9, cpp_type=9, label=2,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='description', full_name='legacy_pb.Metadata.description', index=3,\n      number=4, type=9, cpp_type=9, label=2,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='author', full_name='legacy_pb.Metadata.author', index=4,\n      number=5, type=9, cpp_type=9, label=2,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='license', full_name='legacy_pb.Metadata.license', index=5,\n      number=6, type=9, cpp_type=9, label=2,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='nsfw', full_name='legacy_pb.Metadata.nsfw', index=6,\n      number=7, type=8, cpp_type=7, label=2,\n      has_default_value=False, default_value=False,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='fee', full_name='legacy_pb.Metadata.fee', index=7,\n      number=8, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='thumbnail', full_name='legacy_pb.Metadata.thumbnail', index=8,\n      number=9, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='preview', full_name='legacy_pb.Metadata.preview', index=9,\n      number=10, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='licenseUrl', full_name='legacy_pb.Metadata.licenseUrl', index=10,\n      number=11, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n    _METADATA_VERSION,\n    _METADATA_LANGUAGE,\n  ],\n  serialized_options=None,\n  is_extendable=False,\n  syntax='proto2',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=41,\n  serialized_end=1957,\n)\n\n_METADATA.fields_by_name['version'].enum_type = _METADATA_VERSION\n_METADATA.fields_by_name['language'].enum_type = _METADATA_LANGUAGE\n_METADATA.fields_by_name['fee'].message_type = fee__pb2._FEE\n_METADATA_VERSION.containing_type = _METADATA\n_METADATA_LANGUAGE.containing_type = _METADATA\nDESCRIPTOR.message_types_by_name['Metadata'] = _METADATA\n_sym_db.RegisterFileDescriptor(DESCRIPTOR)\n\nMetadata = _reflection.GeneratedProtocolMessageType('Metadata', (_message.Message,), dict(\n  DESCRIPTOR = _METADATA,\n  __module__ = 'metadata_pb2'\n  # @@protoc_insertion_point(class_scope:legacy_pb.Metadata)\n  ))\n_sym_db.RegisterMessage(Metadata)\n\n\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "lbry/schema/types/v1/signature_pb2.py",
    "content": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: signature.proto\n\nimport sys\n_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import message as _message\nfrom google.protobuf import reflection as _reflection\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\nfrom . import certificate_pb2 as certificate__pb2\n\n\nDESCRIPTOR = _descriptor.FileDescriptor(\n  name='signature.proto',\n  package='legacy_pb',\n  syntax='proto2',\n  serialized_options=None,\n  serialized_pb=_b('\\n\\x0fsignature.proto\\x12\\tlegacy_pb\\x1a\\x11\\x63\\x65rtificate.proto\\\"\\xbb\\x01\\n\\tSignature\\x12-\\n\\x07version\\x18\\x01 \\x02(\\x0e\\x32\\x1c.legacy_pb.Signature.Version\\x12)\\n\\rsignatureType\\x18\\x02 \\x02(\\x0e\\x32\\x12.legacy_pb.KeyType\\x12\\x11\\n\\tsignature\\x18\\x03 \\x02(\\x0c\\x12\\x15\\n\\rcertificateId\\x18\\x04 \\x02(\\x0c\\\"*\\n\\x07Version\\x12\\x13\\n\\x0fUNKNOWN_VERSION\\x10\\x00\\x12\\n\\n\\x06_0_0_1\\x10\\x01')\n  ,\n  dependencies=[certificate__pb2.DESCRIPTOR,])\n\n\n\n_SIGNATURE_VERSION = _descriptor.EnumDescriptor(\n  name='Version',\n  full_name='legacy_pb.Signature.Version',\n  filename=None,\n  file=DESCRIPTOR,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='UNKNOWN_VERSION', index=0, number=0,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='_0_0_1', index=1, number=1,\n      serialized_options=None,\n      type=None),\n  ],\n  containing_type=None,\n  serialized_options=None,\n  serialized_start=195,\n  serialized_end=237,\n)\n_sym_db.RegisterEnumDescriptor(_SIGNATURE_VERSION)\n\n\n_SIGNATURE = _descriptor.Descriptor(\n  name='Signature',\n  full_name='legacy_pb.Signature',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='version', full_name='legacy_pb.Signature.version', index=0,\n      number=1, type=14, cpp_type=8, label=2,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='signatureType', full_name='legacy_pb.Signature.signatureType', index=1,\n      number=2, type=14, cpp_type=8, label=2,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='signature', full_name='legacy_pb.Signature.signature', index=2,\n      number=3, type=12, cpp_type=9, label=2,\n      has_default_value=False, default_value=_b(\"\"),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='certificateId', full_name='legacy_pb.Signature.certificateId', index=3,\n      number=4, type=12, cpp_type=9, label=2,\n      has_default_value=False, default_value=_b(\"\"),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n    _SIGNATURE_VERSION,\n  ],\n  serialized_options=None,\n  is_extendable=False,\n  syntax='proto2',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=50,\n  serialized_end=237,\n)\n\n_SIGNATURE.fields_by_name['version'].enum_type = _SIGNATURE_VERSION\n_SIGNATURE.fields_by_name['signatureType'].enum_type = certificate__pb2._KEYTYPE\n_SIGNATURE_VERSION.containing_type = _SIGNATURE\nDESCRIPTOR.message_types_by_name['Signature'] = _SIGNATURE\n_sym_db.RegisterFileDescriptor(DESCRIPTOR)\n\nSignature = _reflection.GeneratedProtocolMessageType('Signature', (_message.Message,), dict(\n  DESCRIPTOR = _SIGNATURE,\n  __module__ = 'signature_pb2'\n  # @@protoc_insertion_point(class_scope:legacy_pb.Signature)\n  ))\n_sym_db.RegisterMessage(Signature)\n\n\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "lbry/schema/types/v1/source_pb2.py",
    "content": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: source.proto\n\nimport sys\n_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import message as _message\nfrom google.protobuf import reflection as _reflection\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor.FileDescriptor(\n  name='source.proto',\n  package='legacy_pb',\n  syntax='proto2',\n  serialized_options=None,\n  serialized_pb=_b('\\n\\x0csource.proto\\x12\\tlegacy_pb\\\"\\xf2\\x01\\n\\x06Source\\x12*\\n\\x07version\\x18\\x01 \\x02(\\x0e\\x32\\x19.legacy_pb.Source.Version\\x12\\x31\\n\\nsourceType\\x18\\x02 \\x02(\\x0e\\x32\\x1d.legacy_pb.Source.SourceTypes\\x12\\x0e\\n\\x06source\\x18\\x03 \\x02(\\x0c\\x12\\x13\\n\\x0b\\x63ontentType\\x18\\x04 \\x02(\\t\\\"*\\n\\x07Version\\x12\\x13\\n\\x0fUNKNOWN_VERSION\\x10\\x00\\x12\\n\\n\\x06_0_0_1\\x10\\x01\\\"8\\n\\x0bSourceTypes\\x12\\x17\\n\\x13UNKNOWN_SOURCE_TYPE\\x10\\x00\\x12\\x10\\n\\x0clbry_sd_hash\\x10\\x01')\n)\n\n\n\n_SOURCE_VERSION = _descriptor.EnumDescriptor(\n  name='Version',\n  full_name='legacy_pb.Source.Version',\n  filename=None,\n  file=DESCRIPTOR,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='UNKNOWN_VERSION', index=0, number=0,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='_0_0_1', index=1, number=1,\n      serialized_options=None,\n      type=None),\n  ],\n  containing_type=None,\n  serialized_options=None,\n  serialized_start=170,\n  serialized_end=212,\n)\n_sym_db.RegisterEnumDescriptor(_SOURCE_VERSION)\n\n_SOURCE_SOURCETYPES = _descriptor.EnumDescriptor(\n  name='SourceTypes',\n  full_name='legacy_pb.Source.SourceTypes',\n  filename=None,\n  file=DESCRIPTOR,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='UNKNOWN_SOURCE_TYPE', index=0, number=0,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='lbry_sd_hash', index=1, number=1,\n      serialized_options=None,\n      type=None),\n  ],\n  containing_type=None,\n  serialized_options=None,\n  serialized_start=214,\n  serialized_end=270,\n)\n_sym_db.RegisterEnumDescriptor(_SOURCE_SOURCETYPES)\n\n\n_SOURCE = _descriptor.Descriptor(\n  name='Source',\n  full_name='legacy_pb.Source',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='version', full_name='legacy_pb.Source.version', index=0,\n      number=1, type=14, cpp_type=8, label=2,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='sourceType', full_name='legacy_pb.Source.sourceType', index=1,\n      number=2, type=14, cpp_type=8, label=2,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='source', full_name='legacy_pb.Source.source', index=2,\n      number=3, type=12, cpp_type=9, label=2,\n      has_default_value=False, default_value=_b(\"\"),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='contentType', full_name='legacy_pb.Source.contentType', index=3,\n      number=4, type=9, cpp_type=9, label=2,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n    _SOURCE_VERSION,\n    _SOURCE_SOURCETYPES,\n  ],\n  serialized_options=None,\n  is_extendable=False,\n  syntax='proto2',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=28,\n  serialized_end=270,\n)\n\n_SOURCE.fields_by_name['version'].enum_type = _SOURCE_VERSION\n_SOURCE.fields_by_name['sourceType'].enum_type = _SOURCE_SOURCETYPES\n_SOURCE_VERSION.containing_type = _SOURCE\n_SOURCE_SOURCETYPES.containing_type = _SOURCE\nDESCRIPTOR.message_types_by_name['Source'] = _SOURCE\n_sym_db.RegisterFileDescriptor(DESCRIPTOR)\n\nSource = _reflection.GeneratedProtocolMessageType('Source', (_message.Message,), dict(\n  DESCRIPTOR = _SOURCE,\n  __module__ = 'source_pb2'\n  # @@protoc_insertion_point(class_scope:legacy_pb.Source)\n  ))\n_sym_db.RegisterMessage(Source)\n\n\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "lbry/schema/types/v1/stream_pb2.py",
    "content": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: stream.proto\n\nimport sys\n_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import message as _message\nfrom google.protobuf import reflection as _reflection\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\nfrom . import metadata_pb2 as metadata__pb2\nfrom . import source_pb2 as source__pb2\n\n\nDESCRIPTOR = _descriptor.FileDescriptor(\n  name='stream.proto',\n  package='legacy_pb',\n  syntax='proto2',\n  serialized_options=None,\n  serialized_pb=_b('\\n\\x0cstream.proto\\x12\\tlegacy_pb\\x1a\\x0emetadata.proto\\x1a\\x0csource.proto\\\"\\xaa\\x01\\n\\x06Stream\\x12*\\n\\x07version\\x18\\x01 \\x02(\\x0e\\x32\\x19.legacy_pb.Stream.Version\\x12%\\n\\x08metadata\\x18\\x02 \\x02(\\x0b\\x32\\x13.legacy_pb.Metadata\\x12!\\n\\x06source\\x18\\x03 \\x02(\\x0b\\x32\\x11.legacy_pb.Source\\\"*\\n\\x07Version\\x12\\x13\\n\\x0fUNKNOWN_VERSION\\x10\\x00\\x12\\n\\n\\x06_0_0_1\\x10\\x01')\n  ,\n  dependencies=[metadata__pb2.DESCRIPTOR,source__pb2.DESCRIPTOR,])\n\n\n\n_STREAM_VERSION = _descriptor.EnumDescriptor(\n  name='Version',\n  full_name='legacy_pb.Stream.Version',\n  filename=None,\n  file=DESCRIPTOR,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='UNKNOWN_VERSION', index=0, number=0,\n      serialized_options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='_0_0_1', index=1, number=1,\n      serialized_options=None,\n      type=None),\n  ],\n  containing_type=None,\n  serialized_options=None,\n  serialized_start=186,\n  serialized_end=228,\n)\n_sym_db.RegisterEnumDescriptor(_STREAM_VERSION)\n\n\n_STREAM = _descriptor.Descriptor(\n  name='Stream',\n  full_name='legacy_pb.Stream',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='version', full_name='legacy_pb.Stream.version', index=0,\n      number=1, type=14, cpp_type=8, label=2,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='metadata', full_name='legacy_pb.Stream.metadata', index=1,\n      number=2, type=11, cpp_type=10, label=2,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='source', full_name='legacy_pb.Stream.source', index=2,\n      number=3, type=11, cpp_type=10, label=2,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n    _STREAM_VERSION,\n  ],\n  serialized_options=None,\n  is_extendable=False,\n  syntax='proto2',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=58,\n  serialized_end=228,\n)\n\n_STREAM.fields_by_name['version'].enum_type = _STREAM_VERSION\n_STREAM.fields_by_name['metadata'].message_type = metadata__pb2._METADATA\n_STREAM.fields_by_name['source'].message_type = source__pb2._SOURCE\n_STREAM_VERSION.containing_type = _STREAM\nDESCRIPTOR.message_types_by_name['Stream'] = _STREAM\n_sym_db.RegisterFileDescriptor(DESCRIPTOR)\n\nStream = _reflection.GeneratedProtocolMessageType('Stream', (_message.Message,), dict(\n  DESCRIPTOR = _STREAM,\n  __module__ = 'stream_pb2'\n  # @@protoc_insertion_point(class_scope:legacy_pb.Stream)\n  ))\n_sym_db.RegisterMessage(Stream)\n\n\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "lbry/schema/types/v2/__init__.py",
    "content": ""
  },
  {
    "path": "lbry/schema/types/v2/claim_pb2.py",
    "content": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: claim.proto\n\nimport sys\n_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import message as _message\nfrom google.protobuf import reflection as _reflection\nfrom google.protobuf import symbol_database as _symbol_database\nfrom google.protobuf import descriptor_pb2\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor.FileDescriptor(\n  name='claim.proto',\n  package='pb',\n  syntax='proto3',\n  serialized_pb=_b('\\n\\x0b\\x63laim.proto\\x12\\x02pb\\\"\\xab\\x02\\n\\x05\\x43laim\\x12\\x1c\\n\\x06stream\\x18\\x01 \\x01(\\x0b\\x32\\n.pb.StreamH\\x00\\x12\\x1e\\n\\x07\\x63hannel\\x18\\x02 \\x01(\\x0b\\x32\\x0b.pb.ChannelH\\x00\\x12#\\n\\ncollection\\x18\\x03 \\x01(\\x0b\\x32\\r.pb.ClaimListH\\x00\\x12$\\n\\x06repost\\x18\\x04 \\x01(\\x0b\\x32\\x12.pb.ClaimReferenceH\\x00\\x12\\r\\n\\x05title\\x18\\x08 \\x01(\\t\\x12\\x13\\n\\x0b\\x64\\x65scription\\x18\\t \\x01(\\t\\x12\\x1d\\n\\tthumbnail\\x18\\n \\x01(\\x0b\\x32\\n.pb.Source\\x12\\x0c\\n\\x04tags\\x18\\x0b \\x03(\\t\\x12\\x1f\\n\\tlanguages\\x18\\x0c \\x03(\\x0b\\x32\\x0c.pb.Language\\x12\\x1f\\n\\tlocations\\x18\\r \\x03(\\x0b\\x32\\x0c.pb.LocationB\\x06\\n\\x04type\\\"\\x84\\x02\\n\\x06Stream\\x12\\x1a\\n\\x06source\\x18\\x01 \\x01(\\x0b\\x32\\n.pb.Source\\x12\\x0e\\n\\x06\\x61uthor\\x18\\x02 \\x01(\\t\\x12\\x0f\\n\\x07license\\x18\\x03 \\x01(\\t\\x12\\x13\\n\\x0blicense_url\\x18\\x04 \\x01(\\t\\x12\\x14\\n\\x0crelease_time\\x18\\x05 \\x01(\\x03\\x12\\x14\\n\\x03\\x66\\x65\\x65\\x18\\x06 \\x01(\\x0b\\x32\\x07.pb.Fee\\x12\\x1a\\n\\x05image\\x18\\n \\x01(\\x0b\\x32\\t.pb.ImageH\\x00\\x12\\x1a\\n\\x05video\\x18\\x0b \\x01(\\x0b\\x32\\t.pb.VideoH\\x00\\x12\\x1a\\n\\x05\\x61udio\\x18\\x0c \\x01(\\x0b\\x32\\t.pb.AudioH\\x00\\x12 \\n\\x08software\\x18\\r \\x01(\\x0b\\x32\\x0c.pb.SoftwareH\\x00\\x42\\x06\\n\\x04type\\\"}\\n\\x07\\x43hannel\\x12\\x12\\n\\npublic_key\\x18\\x01 \\x01(\\x0c\\x12\\r\\n\\x05\\x65mail\\x18\\x02 \\x01(\\t\\x12\\x13\\n\\x0bwebsite_url\\x18\\x03 \\x01(\\t\\x12\\x19\\n\\x05\\x63over\\x18\\x04 \\x01(\\x0b\\x32\\n.pb.Source\\x12\\x1f\\n\\x08\\x66\\x65\\x61tured\\x18\\x05 \\x01(\\x0b\\x32\\r.pb.ClaimList\\\"$\\n\\x0e\\x43laimReference\\x12\\x12\\n\\nclaim_hash\\x18\\x01 \\x01(\\x0c\\\"\\x90\\x01\\n\\tClaimList\\x12)\\n\\tlist_type\\x18\\x01 \\x01(\\x0e\\x32\\x16.pb.ClaimList.ListType\\x12,\\n\\x10\\x63laim_references\\x18\\x02 \\x03(\\x0b\\x32\\x12.pb.ClaimReference\\\"*\\n\\x08ListType\\x12\\x0e\\n\\nCOLLECTION\\x10\\x00\\x12\\x0e\\n\\nDERIVATION\\x10\\x02\\\"y\\n\\x06Source\\x12\\x0c\\n\\x04hash\\x18\\x01 \\x01(\\x0c\\x12\\x0c\\n\\x04name\\x18\\x02 \\x01(\\t\\x12\\x0c\\n\\x04size\\x18\\x03 \\x01(\\x04\\x12\\x12\\n\\nmedia_type\\x18\\x04 \\x01(\\t\\x12\\x0b\\n\\x03url\\x18\\x05 \\x01(\\t\\x12\\x0f\\n\\x07sd_hash\\x18\\x06 \\x01(\\x0c\\x12\\x13\\n\\x0b\\x62t_infohash\\x18\\x07 \\x01(\\x0c\\\"\\x87\\x01\\n\\x03\\x46\\x65\\x65\\x12\\\"\\n\\x08\\x63urrency\\x18\\x01 \\x01(\\x0e\\x32\\x10.pb.Fee.Currency\\x12\\x0f\\n\\x07\\x61\\x64\\x64ress\\x18\\x02 \\x01(\\x0c\\x12\\x0e\\n\\x06\\x61mount\\x18\\x03 \\x01(\\x04\\\";\\n\\x08\\x43urrency\\x12\\x14\\n\\x10UNKNOWN_CURRENCY\\x10\\x00\\x12\\x07\\n\\x03LBC\\x10\\x01\\x12\\x07\\n\\x03\\x42TC\\x10\\x02\\x12\\x07\\n\\x03USD\\x10\\x03\\\"&\\n\\x05Image\\x12\\r\\n\\x05width\\x18\\x01 \\x01(\\r\\x12\\x0e\\n\\x06height\\x18\\x02 \\x01(\\r\\\"R\\n\\x05Video\\x12\\r\\n\\x05width\\x18\\x01 \\x01(\\r\\x12\\x0e\\n\\x06height\\x18\\x02 \\x01(\\r\\x12\\x10\\n\\x08\\x64uration\\x18\\x03 \\x01(\\r\\x12\\x18\\n\\x05\\x61udio\\x18\\x0f \\x01(\\x0b\\x32\\t.pb.Audio\\\"\\x19\\n\\x05\\x41udio\\x12\\x10\\n\\x08\\x64uration\\x18\\x01 \\x01(\\r\\\"l\\n\\x08Software\\x12\\n\\n\\x02os\\x18\\x01 \\x01(\\t\\\"T\\n\\x02OS\\x12\\x0e\\n\\nUNKNOWN_OS\\x10\\x00\\x12\\x07\\n\\x03\\x41NY\\x10\\x01\\x12\\t\\n\\x05LINUX\\x10\\x02\\x12\\x0b\\n\\x07WINDOWS\\x10\\x03\\x12\\x07\\n\\x03MAC\\x10\\x04\\x12\\x0b\\n\\x07\\x41NDROID\\x10\\x05\\x12\\x07\\n\\x03IOS\\x10\\x06\\\"\\xc7\\x1d\\n\\x08Language\\x12\\'\\n\\x08language\\x18\\x01 \\x01(\\x0e\\x32\\x15.pb.Language.Language\\x12#\\n\\x06script\\x18\\x02 \\x01(\\x0e\\x32\\x13.pb.Language.Script\\x12$\\n\\x06region\\x18\\x03 \\x01(\\x0e\\x32\\x14.pb.Location.Country\\\"\\x99\\x0c\\n\\x08Language\\x12\\x14\\n\\x10UNKNOWN_LANGUAGE\\x10\\x00\\x12\\x06\\n\\x02\\x65n\\x10\\x01\\x12\\x06\\n\\x02\\x61\\x61\\x10\\x02\\x12\\x06\\n\\x02\\x61\\x62\\x10\\x03\\x12\\x06\\n\\x02\\x61\\x65\\x10\\x04\\x12\\x06\\n\\x02\\x61\\x66\\x10\\x05\\x12\\x06\\n\\x02\\x61k\\x10\\x06\\x12\\x06\\n\\x02\\x61m\\x10\\x07\\x12\\x06\\n\\x02\\x61n\\x10\\x08\\x12\\x06\\n\\x02\\x61r\\x10\\t\\x12\\x06\\n\\x02\\x61s\\x10\\n\\x12\\x06\\n\\x02\\x61v\\x10\\x0b\\x12\\x06\\n\\x02\\x61y\\x10\\x0c\\x12\\x06\\n\\x02\\x61z\\x10\\r\\x12\\x06\\n\\x02\\x62\\x61\\x10\\x0e\\x12\\x06\\n\\x02\\x62\\x65\\x10\\x0f\\x12\\x06\\n\\x02\\x62g\\x10\\x10\\x12\\x06\\n\\x02\\x62h\\x10\\x11\\x12\\x06\\n\\x02\\x62i\\x10\\x12\\x12\\x06\\n\\x02\\x62m\\x10\\x13\\x12\\x06\\n\\x02\\x62n\\x10\\x14\\x12\\x06\\n\\x02\\x62o\\x10\\x15\\x12\\x06\\n\\x02\\x62r\\x10\\x16\\x12\\x06\\n\\x02\\x62s\\x10\\x17\\x12\\x06\\n\\x02\\x63\\x61\\x10\\x18\\x12\\x06\\n\\x02\\x63\\x65\\x10\\x19\\x12\\x06\\n\\x02\\x63h\\x10\\x1a\\x12\\x06\\n\\x02\\x63o\\x10\\x1b\\x12\\x06\\n\\x02\\x63r\\x10\\x1c\\x12\\x06\\n\\x02\\x63s\\x10\\x1d\\x12\\x06\\n\\x02\\x63u\\x10\\x1e\\x12\\x06\\n\\x02\\x63v\\x10\\x1f\\x12\\x06\\n\\x02\\x63y\\x10 \\x12\\x06\\n\\x02\\x64\\x61\\x10!\\x12\\x06\\n\\x02\\x64\\x65\\x10\\\"\\x12\\x06\\n\\x02\\x64v\\x10#\\x12\\x06\\n\\x02\\x64z\\x10$\\x12\\x06\\n\\x02\\x65\\x65\\x10%\\x12\\x06\\n\\x02\\x65l\\x10&\\x12\\x06\\n\\x02\\x65o\\x10\\'\\x12\\x06\\n\\x02\\x65s\\x10(\\x12\\x06\\n\\x02\\x65t\\x10)\\x12\\x06\\n\\x02\\x65u\\x10*\\x12\\x06\\n\\x02\\x66\\x61\\x10+\\x12\\x06\\n\\x02\\x66\\x66\\x10,\\x12\\x06\\n\\x02\\x66i\\x10-\\x12\\x06\\n\\x02\\x66j\\x10.\\x12\\x06\\n\\x02\\x66o\\x10/\\x12\\x06\\n\\x02\\x66r\\x10\\x30\\x12\\x06\\n\\x02\\x66y\\x10\\x31\\x12\\x06\\n\\x02ga\\x10\\x32\\x12\\x06\\n\\x02gd\\x10\\x33\\x12\\x06\\n\\x02gl\\x10\\x34\\x12\\x06\\n\\x02gn\\x10\\x35\\x12\\x06\\n\\x02gu\\x10\\x36\\x12\\x06\\n\\x02gv\\x10\\x37\\x12\\x06\\n\\x02ha\\x10\\x38\\x12\\x06\\n\\x02he\\x10\\x39\\x12\\x06\\n\\x02hi\\x10:\\x12\\x06\\n\\x02ho\\x10;\\x12\\x06\\n\\x02hr\\x10<\\x12\\x06\\n\\x02ht\\x10=\\x12\\x06\\n\\x02hu\\x10>\\x12\\x06\\n\\x02hy\\x10?\\x12\\x06\\n\\x02hz\\x10@\\x12\\x06\\n\\x02ia\\x10\\x41\\x12\\x06\\n\\x02id\\x10\\x42\\x12\\x06\\n\\x02ie\\x10\\x43\\x12\\x06\\n\\x02ig\\x10\\x44\\x12\\x06\\n\\x02ii\\x10\\x45\\x12\\x06\\n\\x02ik\\x10\\x46\\x12\\x06\\n\\x02io\\x10G\\x12\\x06\\n\\x02is\\x10H\\x12\\x06\\n\\x02it\\x10I\\x12\\x06\\n\\x02iu\\x10J\\x12\\x06\\n\\x02ja\\x10K\\x12\\x06\\n\\x02jv\\x10L\\x12\\x06\\n\\x02ka\\x10M\\x12\\x06\\n\\x02kg\\x10N\\x12\\x06\\n\\x02ki\\x10O\\x12\\x06\\n\\x02kj\\x10P\\x12\\x06\\n\\x02kk\\x10Q\\x12\\x06\\n\\x02kl\\x10R\\x12\\x06\\n\\x02km\\x10S\\x12\\x06\\n\\x02kn\\x10T\\x12\\x06\\n\\x02ko\\x10U\\x12\\x06\\n\\x02kr\\x10V\\x12\\x06\\n\\x02ks\\x10W\\x12\\x06\\n\\x02ku\\x10X\\x12\\x06\\n\\x02kv\\x10Y\\x12\\x06\\n\\x02kw\\x10Z\\x12\\x06\\n\\x02ky\\x10[\\x12\\x06\\n\\x02la\\x10\\\\\\x12\\x06\\n\\x02lb\\x10]\\x12\\x06\\n\\x02lg\\x10^\\x12\\x06\\n\\x02li\\x10_\\x12\\x06\\n\\x02ln\\x10`\\x12\\x06\\n\\x02lo\\x10\\x61\\x12\\x06\\n\\x02lt\\x10\\x62\\x12\\x06\\n\\x02lu\\x10\\x63\\x12\\x06\\n\\x02lv\\x10\\x64\\x12\\x06\\n\\x02mg\\x10\\x65\\x12\\x06\\n\\x02mh\\x10\\x66\\x12\\x06\\n\\x02mi\\x10g\\x12\\x06\\n\\x02mk\\x10h\\x12\\x06\\n\\x02ml\\x10i\\x12\\x06\\n\\x02mn\\x10j\\x12\\x06\\n\\x02mr\\x10k\\x12\\x06\\n\\x02ms\\x10l\\x12\\x06\\n\\x02mt\\x10m\\x12\\x06\\n\\x02my\\x10n\\x12\\x06\\n\\x02na\\x10o\\x12\\x06\\n\\x02nb\\x10p\\x12\\x06\\n\\x02nd\\x10q\\x12\\x06\\n\\x02ne\\x10r\\x12\\x06\\n\\x02ng\\x10s\\x12\\x06\\n\\x02nl\\x10t\\x12\\x06\\n\\x02nn\\x10u\\x12\\x06\\n\\x02no\\x10v\\x12\\x06\\n\\x02nr\\x10w\\x12\\x06\\n\\x02nv\\x10x\\x12\\x06\\n\\x02ny\\x10y\\x12\\x06\\n\\x02oc\\x10z\\x12\\x06\\n\\x02oj\\x10{\\x12\\x06\\n\\x02om\\x10|\\x12\\x06\\n\\x02or\\x10}\\x12\\x06\\n\\x02os\\x10~\\x12\\x06\\n\\x02pa\\x10\\x7f\\x12\\x07\\n\\x02pi\\x10\\x80\\x01\\x12\\x07\\n\\x02pl\\x10\\x81\\x01\\x12\\x07\\n\\x02ps\\x10\\x82\\x01\\x12\\x07\\n\\x02pt\\x10\\x83\\x01\\x12\\x07\\n\\x02qu\\x10\\x84\\x01\\x12\\x07\\n\\x02rm\\x10\\x85\\x01\\x12\\x07\\n\\x02rn\\x10\\x86\\x01\\x12\\x07\\n\\x02ro\\x10\\x87\\x01\\x12\\x07\\n\\x02ru\\x10\\x88\\x01\\x12\\x07\\n\\x02rw\\x10\\x89\\x01\\x12\\x07\\n\\x02sa\\x10\\x8a\\x01\\x12\\x07\\n\\x02sc\\x10\\x8b\\x01\\x12\\x07\\n\\x02sd\\x10\\x8c\\x01\\x12\\x07\\n\\x02se\\x10\\x8d\\x01\\x12\\x07\\n\\x02sg\\x10\\x8e\\x01\\x12\\x07\\n\\x02si\\x10\\x8f\\x01\\x12\\x07\\n\\x02sk\\x10\\x90\\x01\\x12\\x07\\n\\x02sl\\x10\\x91\\x01\\x12\\x07\\n\\x02sm\\x10\\x92\\x01\\x12\\x07\\n\\x02sn\\x10\\x93\\x01\\x12\\x07\\n\\x02so\\x10\\x94\\x01\\x12\\x07\\n\\x02sq\\x10\\x95\\x01\\x12\\x07\\n\\x02sr\\x10\\x96\\x01\\x12\\x07\\n\\x02ss\\x10\\x97\\x01\\x12\\x07\\n\\x02st\\x10\\x98\\x01\\x12\\x07\\n\\x02su\\x10\\x99\\x01\\x12\\x07\\n\\x02sv\\x10\\x9a\\x01\\x12\\x07\\n\\x02sw\\x10\\x9b\\x01\\x12\\x07\\n\\x02ta\\x10\\x9c\\x01\\x12\\x07\\n\\x02te\\x10\\x9d\\x01\\x12\\x07\\n\\x02tg\\x10\\x9e\\x01\\x12\\x07\\n\\x02th\\x10\\x9f\\x01\\x12\\x07\\n\\x02ti\\x10\\xa0\\x01\\x12\\x07\\n\\x02tk\\x10\\xa1\\x01\\x12\\x07\\n\\x02tl\\x10\\xa2\\x01\\x12\\x07\\n\\x02tn\\x10\\xa3\\x01\\x12\\x07\\n\\x02to\\x10\\xa4\\x01\\x12\\x07\\n\\x02tr\\x10\\xa5\\x01\\x12\\x07\\n\\x02ts\\x10\\xa6\\x01\\x12\\x07\\n\\x02tt\\x10\\xa7\\x01\\x12\\x07\\n\\x02tw\\x10\\xa8\\x01\\x12\\x07\\n\\x02ty\\x10\\xa9\\x01\\x12\\x07\\n\\x02ug\\x10\\xaa\\x01\\x12\\x07\\n\\x02uk\\x10\\xab\\x01\\x12\\x07\\n\\x02ur\\x10\\xac\\x01\\x12\\x07\\n\\x02uz\\x10\\xad\\x01\\x12\\x07\\n\\x02ve\\x10\\xae\\x01\\x12\\x07\\n\\x02vi\\x10\\xaf\\x01\\x12\\x07\\n\\x02vo\\x10\\xb0\\x01\\x12\\x07\\n\\x02wa\\x10\\xb1\\x01\\x12\\x07\\n\\x02wo\\x10\\xb2\\x01\\x12\\x07\\n\\x02xh\\x10\\xb3\\x01\\x12\\x07\\n\\x02yi\\x10\\xb4\\x01\\x12\\x07\\n\\x02yo\\x10\\xb5\\x01\\x12\\x07\\n\\x02za\\x10\\xb6\\x01\\x12\\x07\\n\\x02zh\\x10\\xb7\\x01\\x12\\x07\\n\\x02zu\\x10\\xb8\\x01\\\"\\xaa\\x10\\n\\x06Script\\x12\\x12\\n\\x0eUNKNOWN_SCRIPT\\x10\\x00\\x12\\x08\\n\\x04\\x41\\x64lm\\x10\\x01\\x12\\x08\\n\\x04\\x41\\x66\\x61k\\x10\\x02\\x12\\x08\\n\\x04\\x41ghb\\x10\\x03\\x12\\x08\\n\\x04\\x41hom\\x10\\x04\\x12\\x08\\n\\x04\\x41rab\\x10\\x05\\x12\\x08\\n\\x04\\x41ran\\x10\\x06\\x12\\x08\\n\\x04\\x41rmi\\x10\\x07\\x12\\x08\\n\\x04\\x41rmn\\x10\\x08\\x12\\x08\\n\\x04\\x41vst\\x10\\t\\x12\\x08\\n\\x04\\x42\\x61li\\x10\\n\\x12\\x08\\n\\x04\\x42\\x61mu\\x10\\x0b\\x12\\x08\\n\\x04\\x42\\x61ss\\x10\\x0c\\x12\\x08\\n\\x04\\x42\\x61tk\\x10\\r\\x12\\x08\\n\\x04\\x42\\x65ng\\x10\\x0e\\x12\\x08\\n\\x04\\x42hks\\x10\\x0f\\x12\\x08\\n\\x04\\x42lis\\x10\\x10\\x12\\x08\\n\\x04\\x42opo\\x10\\x11\\x12\\x08\\n\\x04\\x42rah\\x10\\x12\\x12\\x08\\n\\x04\\x42rai\\x10\\x13\\x12\\x08\\n\\x04\\x42ugi\\x10\\x14\\x12\\x08\\n\\x04\\x42uhd\\x10\\x15\\x12\\x08\\n\\x04\\x43\\x61km\\x10\\x16\\x12\\x08\\n\\x04\\x43\\x61ns\\x10\\x17\\x12\\x08\\n\\x04\\x43\\x61ri\\x10\\x18\\x12\\x08\\n\\x04\\x43ham\\x10\\x19\\x12\\x08\\n\\x04\\x43her\\x10\\x1a\\x12\\x08\\n\\x04\\x43irt\\x10\\x1b\\x12\\x08\\n\\x04\\x43opt\\x10\\x1c\\x12\\x08\\n\\x04\\x43pmn\\x10\\x1d\\x12\\x08\\n\\x04\\x43prt\\x10\\x1e\\x12\\x08\\n\\x04\\x43yrl\\x10\\x1f\\x12\\x08\\n\\x04\\x43yrs\\x10 \\x12\\x08\\n\\x04\\x44\\x65va\\x10!\\x12\\x08\\n\\x04\\x44ogr\\x10\\\"\\x12\\x08\\n\\x04\\x44srt\\x10#\\x12\\x08\\n\\x04\\x44upl\\x10$\\x12\\x08\\n\\x04\\x45gyd\\x10%\\x12\\x08\\n\\x04\\x45gyh\\x10&\\x12\\x08\\n\\x04\\x45gyp\\x10\\'\\x12\\x08\\n\\x04\\x45lba\\x10(\\x12\\x08\\n\\x04\\x45lym\\x10)\\x12\\x08\\n\\x04\\x45thi\\x10*\\x12\\x08\\n\\x04Geok\\x10+\\x12\\x08\\n\\x04Geor\\x10,\\x12\\x08\\n\\x04Glag\\x10-\\x12\\x08\\n\\x04Gong\\x10.\\x12\\x08\\n\\x04Gonm\\x10/\\x12\\x08\\n\\x04Goth\\x10\\x30\\x12\\x08\\n\\x04Gran\\x10\\x31\\x12\\x08\\n\\x04Grek\\x10\\x32\\x12\\x08\\n\\x04Gujr\\x10\\x33\\x12\\x08\\n\\x04Guru\\x10\\x34\\x12\\x08\\n\\x04Hanb\\x10\\x35\\x12\\x08\\n\\x04Hang\\x10\\x36\\x12\\x08\\n\\x04Hani\\x10\\x37\\x12\\x08\\n\\x04Hano\\x10\\x38\\x12\\x08\\n\\x04Hans\\x10\\x39\\x12\\x08\\n\\x04Hant\\x10:\\x12\\x08\\n\\x04Hatr\\x10;\\x12\\x08\\n\\x04Hebr\\x10<\\x12\\x08\\n\\x04Hira\\x10=\\x12\\x08\\n\\x04Hluw\\x10>\\x12\\x08\\n\\x04Hmng\\x10?\\x12\\x08\\n\\x04Hmnp\\x10@\\x12\\x08\\n\\x04Hrkt\\x10\\x41\\x12\\x08\\n\\x04Hung\\x10\\x42\\x12\\x08\\n\\x04Inds\\x10\\x43\\x12\\x08\\n\\x04Ital\\x10\\x44\\x12\\x08\\n\\x04Jamo\\x10\\x45\\x12\\x08\\n\\x04Java\\x10\\x46\\x12\\x08\\n\\x04Jpan\\x10G\\x12\\x08\\n\\x04Jurc\\x10H\\x12\\x08\\n\\x04Kali\\x10I\\x12\\x08\\n\\x04Kana\\x10J\\x12\\x08\\n\\x04Khar\\x10K\\x12\\x08\\n\\x04Khmr\\x10L\\x12\\x08\\n\\x04Khoj\\x10M\\x12\\x08\\n\\x04Kitl\\x10N\\x12\\x08\\n\\x04Kits\\x10O\\x12\\x08\\n\\x04Knda\\x10P\\x12\\x08\\n\\x04Kore\\x10Q\\x12\\x08\\n\\x04Kpel\\x10R\\x12\\x08\\n\\x04Kthi\\x10S\\x12\\x08\\n\\x04Lana\\x10T\\x12\\x08\\n\\x04Laoo\\x10U\\x12\\x08\\n\\x04Latf\\x10V\\x12\\x08\\n\\x04Latg\\x10W\\x12\\x08\\n\\x04Latn\\x10X\\x12\\x08\\n\\x04Leke\\x10Y\\x12\\x08\\n\\x04Lepc\\x10Z\\x12\\x08\\n\\x04Limb\\x10[\\x12\\x08\\n\\x04Lina\\x10\\\\\\x12\\x08\\n\\x04Linb\\x10]\\x12\\x08\\n\\x04Lisu\\x10^\\x12\\x08\\n\\x04Loma\\x10_\\x12\\x08\\n\\x04Lyci\\x10`\\x12\\x08\\n\\x04Lydi\\x10\\x61\\x12\\x08\\n\\x04Mahj\\x10\\x62\\x12\\x08\\n\\x04Maka\\x10\\x63\\x12\\x08\\n\\x04Mand\\x10\\x64\\x12\\x08\\n\\x04Mani\\x10\\x65\\x12\\x08\\n\\x04Marc\\x10\\x66\\x12\\x08\\n\\x04Maya\\x10g\\x12\\x08\\n\\x04Medf\\x10h\\x12\\x08\\n\\x04Mend\\x10i\\x12\\x08\\n\\x04Merc\\x10j\\x12\\x08\\n\\x04Mero\\x10k\\x12\\x08\\n\\x04Mlym\\x10l\\x12\\x08\\n\\x04Modi\\x10m\\x12\\x08\\n\\x04Mong\\x10n\\x12\\x08\\n\\x04Moon\\x10o\\x12\\x08\\n\\x04Mroo\\x10p\\x12\\x08\\n\\x04Mtei\\x10q\\x12\\x08\\n\\x04Mult\\x10r\\x12\\x08\\n\\x04Mymr\\x10s\\x12\\x08\\n\\x04Nand\\x10t\\x12\\x08\\n\\x04Narb\\x10u\\x12\\x08\\n\\x04Nbat\\x10v\\x12\\x08\\n\\x04Newa\\x10w\\x12\\x08\\n\\x04Nkdb\\x10x\\x12\\x08\\n\\x04Nkgb\\x10y\\x12\\x08\\n\\x04Nkoo\\x10z\\x12\\x08\\n\\x04Nshu\\x10{\\x12\\x08\\n\\x04Ogam\\x10|\\x12\\x08\\n\\x04Olck\\x10}\\x12\\x08\\n\\x04Orkh\\x10~\\x12\\x08\\n\\x04Orya\\x10\\x7f\\x12\\t\\n\\x04Osge\\x10\\x80\\x01\\x12\\t\\n\\x04Osma\\x10\\x81\\x01\\x12\\t\\n\\x04Palm\\x10\\x82\\x01\\x12\\t\\n\\x04Pauc\\x10\\x83\\x01\\x12\\t\\n\\x04Perm\\x10\\x84\\x01\\x12\\t\\n\\x04Phag\\x10\\x85\\x01\\x12\\t\\n\\x04Phli\\x10\\x86\\x01\\x12\\t\\n\\x04Phlp\\x10\\x87\\x01\\x12\\t\\n\\x04Phlv\\x10\\x88\\x01\\x12\\t\\n\\x04Phnx\\x10\\x89\\x01\\x12\\t\\n\\x04Plrd\\x10\\x8a\\x01\\x12\\t\\n\\x04Piqd\\x10\\x8b\\x01\\x12\\t\\n\\x04Prti\\x10\\x8c\\x01\\x12\\t\\n\\x04Qaaa\\x10\\x8d\\x01\\x12\\t\\n\\x04Qabx\\x10\\x8e\\x01\\x12\\t\\n\\x04Rjng\\x10\\x8f\\x01\\x12\\t\\n\\x04Rohg\\x10\\x90\\x01\\x12\\t\\n\\x04Roro\\x10\\x91\\x01\\x12\\t\\n\\x04Runr\\x10\\x92\\x01\\x12\\t\\n\\x04Samr\\x10\\x93\\x01\\x12\\t\\n\\x04Sara\\x10\\x94\\x01\\x12\\t\\n\\x04Sarb\\x10\\x95\\x01\\x12\\t\\n\\x04Saur\\x10\\x96\\x01\\x12\\t\\n\\x04Sgnw\\x10\\x97\\x01\\x12\\t\\n\\x04Shaw\\x10\\x98\\x01\\x12\\t\\n\\x04Shrd\\x10\\x99\\x01\\x12\\t\\n\\x04Shui\\x10\\x9a\\x01\\x12\\t\\n\\x04Sidd\\x10\\x9b\\x01\\x12\\t\\n\\x04Sind\\x10\\x9c\\x01\\x12\\t\\n\\x04Sinh\\x10\\x9d\\x01\\x12\\t\\n\\x04Sogd\\x10\\x9e\\x01\\x12\\t\\n\\x04Sogo\\x10\\x9f\\x01\\x12\\t\\n\\x04Sora\\x10\\xa0\\x01\\x12\\t\\n\\x04Soyo\\x10\\xa1\\x01\\x12\\t\\n\\x04Sund\\x10\\xa2\\x01\\x12\\t\\n\\x04Sylo\\x10\\xa3\\x01\\x12\\t\\n\\x04Syrc\\x10\\xa4\\x01\\x12\\t\\n\\x04Syre\\x10\\xa5\\x01\\x12\\t\\n\\x04Syrj\\x10\\xa6\\x01\\x12\\t\\n\\x04Syrn\\x10\\xa7\\x01\\x12\\t\\n\\x04Tagb\\x10\\xa8\\x01\\x12\\t\\n\\x04Takr\\x10\\xa9\\x01\\x12\\t\\n\\x04Tale\\x10\\xaa\\x01\\x12\\t\\n\\x04Talu\\x10\\xab\\x01\\x12\\t\\n\\x04Taml\\x10\\xac\\x01\\x12\\t\\n\\x04Tang\\x10\\xad\\x01\\x12\\t\\n\\x04Tavt\\x10\\xae\\x01\\x12\\t\\n\\x04Telu\\x10\\xaf\\x01\\x12\\t\\n\\x04Teng\\x10\\xb0\\x01\\x12\\t\\n\\x04Tfng\\x10\\xb1\\x01\\x12\\t\\n\\x04Tglg\\x10\\xb2\\x01\\x12\\t\\n\\x04Thaa\\x10\\xb3\\x01\\x12\\t\\n\\x04Thai\\x10\\xb4\\x01\\x12\\t\\n\\x04Tibt\\x10\\xb5\\x01\\x12\\t\\n\\x04Tirh\\x10\\xb6\\x01\\x12\\t\\n\\x04Ugar\\x10\\xb7\\x01\\x12\\t\\n\\x04Vaii\\x10\\xb8\\x01\\x12\\t\\n\\x04Visp\\x10\\xb9\\x01\\x12\\t\\n\\x04Wara\\x10\\xba\\x01\\x12\\t\\n\\x04Wcho\\x10\\xbb\\x01\\x12\\t\\n\\x04Wole\\x10\\xbc\\x01\\x12\\t\\n\\x04Xpeo\\x10\\xbd\\x01\\x12\\t\\n\\x04Xsux\\x10\\xbe\\x01\\x12\\t\\n\\x04Yiii\\x10\\xbf\\x01\\x12\\t\\n\\x04Zanb\\x10\\xc0\\x01\\x12\\t\\n\\x04Zinh\\x10\\xc1\\x01\\x12\\t\\n\\x04Zmth\\x10\\xc2\\x01\\x12\\t\\n\\x04Zsye\\x10\\xc3\\x01\\x12\\t\\n\\x04Zsym\\x10\\xc4\\x01\\x12\\t\\n\\x04Zxxx\\x10\\xc5\\x01\\x12\\t\\n\\x04Zyyy\\x10\\xc6\\x01\\x12\\t\\n\\x04Zzzz\\x10\\xc7\\x01\\\"\\xec)\\n\\x08Location\\x12%\\n\\x07\\x63ountry\\x18\\x01 \\x01(\\x0e\\x32\\x14.pb.Location.Country\\x12\\r\\n\\x05state\\x18\\x02 \\x01(\\t\\x12\\x0c\\n\\x04\\x63ity\\x18\\x03 \\x01(\\t\\x12\\x0c\\n\\x04\\x63ode\\x18\\x04 \\x01(\\t\\x12\\x10\\n\\x08latitude\\x18\\x05 \\x01(\\x11\\x12\\x11\\n\\tlongitude\\x18\\x06 \\x01(\\x11\\\"\\xe8(\\n\\x07\\x43ountry\\x12\\x13\\n\\x0fUNKNOWN_COUNTRY\\x10\\x00\\x12\\x06\\n\\x02\\x41\\x46\\x10\\x01\\x12\\x06\\n\\x02\\x41X\\x10\\x02\\x12\\x06\\n\\x02\\x41L\\x10\\x03\\x12\\x06\\n\\x02\\x44Z\\x10\\x04\\x12\\x06\\n\\x02\\x41S\\x10\\x05\\x12\\x06\\n\\x02\\x41\\x44\\x10\\x06\\x12\\x06\\n\\x02\\x41O\\x10\\x07\\x12\\x06\\n\\x02\\x41I\\x10\\x08\\x12\\x06\\n\\x02\\x41Q\\x10\\t\\x12\\x06\\n\\x02\\x41G\\x10\\n\\x12\\x06\\n\\x02\\x41R\\x10\\x0b\\x12\\x06\\n\\x02\\x41M\\x10\\x0c\\x12\\x06\\n\\x02\\x41W\\x10\\r\\x12\\x06\\n\\x02\\x41U\\x10\\x0e\\x12\\x06\\n\\x02\\x41T\\x10\\x0f\\x12\\x06\\n\\x02\\x41Z\\x10\\x10\\x12\\x06\\n\\x02\\x42S\\x10\\x11\\x12\\x06\\n\\x02\\x42H\\x10\\x12\\x12\\x06\\n\\x02\\x42\\x44\\x10\\x13\\x12\\x06\\n\\x02\\x42\\x42\\x10\\x14\\x12\\x06\\n\\x02\\x42Y\\x10\\x15\\x12\\x06\\n\\x02\\x42\\x45\\x10\\x16\\x12\\x06\\n\\x02\\x42Z\\x10\\x17\\x12\\x06\\n\\x02\\x42J\\x10\\x18\\x12\\x06\\n\\x02\\x42M\\x10\\x19\\x12\\x06\\n\\x02\\x42T\\x10\\x1a\\x12\\x06\\n\\x02\\x42O\\x10\\x1b\\x12\\x06\\n\\x02\\x42Q\\x10\\x1c\\x12\\x06\\n\\x02\\x42\\x41\\x10\\x1d\\x12\\x06\\n\\x02\\x42W\\x10\\x1e\\x12\\x06\\n\\x02\\x42V\\x10\\x1f\\x12\\x06\\n\\x02\\x42R\\x10 \\x12\\x06\\n\\x02IO\\x10!\\x12\\x06\\n\\x02\\x42N\\x10\\\"\\x12\\x06\\n\\x02\\x42G\\x10#\\x12\\x06\\n\\x02\\x42\\x46\\x10$\\x12\\x06\\n\\x02\\x42I\\x10%\\x12\\x06\\n\\x02KH\\x10&\\x12\\x06\\n\\x02\\x43M\\x10\\'\\x12\\x06\\n\\x02\\x43\\x41\\x10(\\x12\\x06\\n\\x02\\x43V\\x10)\\x12\\x06\\n\\x02KY\\x10*\\x12\\x06\\n\\x02\\x43\\x46\\x10+\\x12\\x06\\n\\x02TD\\x10,\\x12\\x06\\n\\x02\\x43L\\x10-\\x12\\x06\\n\\x02\\x43N\\x10.\\x12\\x06\\n\\x02\\x43X\\x10/\\x12\\x06\\n\\x02\\x43\\x43\\x10\\x30\\x12\\x06\\n\\x02\\x43O\\x10\\x31\\x12\\x06\\n\\x02KM\\x10\\x32\\x12\\x06\\n\\x02\\x43G\\x10\\x33\\x12\\x06\\n\\x02\\x43\\x44\\x10\\x34\\x12\\x06\\n\\x02\\x43K\\x10\\x35\\x12\\x06\\n\\x02\\x43R\\x10\\x36\\x12\\x06\\n\\x02\\x43I\\x10\\x37\\x12\\x06\\n\\x02HR\\x10\\x38\\x12\\x06\\n\\x02\\x43U\\x10\\x39\\x12\\x06\\n\\x02\\x43W\\x10:\\x12\\x06\\n\\x02\\x43Y\\x10;\\x12\\x06\\n\\x02\\x43Z\\x10<\\x12\\x06\\n\\x02\\x44K\\x10=\\x12\\x06\\n\\x02\\x44J\\x10>\\x12\\x06\\n\\x02\\x44M\\x10?\\x12\\x06\\n\\x02\\x44O\\x10@\\x12\\x06\\n\\x02\\x45\\x43\\x10\\x41\\x12\\x06\\n\\x02\\x45G\\x10\\x42\\x12\\x06\\n\\x02SV\\x10\\x43\\x12\\x06\\n\\x02GQ\\x10\\x44\\x12\\x06\\n\\x02\\x45R\\x10\\x45\\x12\\x06\\n\\x02\\x45\\x45\\x10\\x46\\x12\\x06\\n\\x02\\x45T\\x10G\\x12\\x06\\n\\x02\\x46K\\x10H\\x12\\x06\\n\\x02\\x46O\\x10I\\x12\\x06\\n\\x02\\x46J\\x10J\\x12\\x06\\n\\x02\\x46I\\x10K\\x12\\x06\\n\\x02\\x46R\\x10L\\x12\\x06\\n\\x02GF\\x10M\\x12\\x06\\n\\x02PF\\x10N\\x12\\x06\\n\\x02TF\\x10O\\x12\\x06\\n\\x02GA\\x10P\\x12\\x06\\n\\x02GM\\x10Q\\x12\\x06\\n\\x02GE\\x10R\\x12\\x06\\n\\x02\\x44\\x45\\x10S\\x12\\x06\\n\\x02GH\\x10T\\x12\\x06\\n\\x02GI\\x10U\\x12\\x06\\n\\x02GR\\x10V\\x12\\x06\\n\\x02GL\\x10W\\x12\\x06\\n\\x02GD\\x10X\\x12\\x06\\n\\x02GP\\x10Y\\x12\\x06\\n\\x02GU\\x10Z\\x12\\x06\\n\\x02GT\\x10[\\x12\\x06\\n\\x02GG\\x10\\\\\\x12\\x06\\n\\x02GN\\x10]\\x12\\x06\\n\\x02GW\\x10^\\x12\\x06\\n\\x02GY\\x10_\\x12\\x06\\n\\x02HT\\x10`\\x12\\x06\\n\\x02HM\\x10\\x61\\x12\\x06\\n\\x02VA\\x10\\x62\\x12\\x06\\n\\x02HN\\x10\\x63\\x12\\x06\\n\\x02HK\\x10\\x64\\x12\\x06\\n\\x02HU\\x10\\x65\\x12\\x06\\n\\x02IS\\x10\\x66\\x12\\x06\\n\\x02IN\\x10g\\x12\\x06\\n\\x02ID\\x10h\\x12\\x06\\n\\x02IR\\x10i\\x12\\x06\\n\\x02IQ\\x10j\\x12\\x06\\n\\x02IE\\x10k\\x12\\x06\\n\\x02IM\\x10l\\x12\\x06\\n\\x02IL\\x10m\\x12\\x06\\n\\x02IT\\x10n\\x12\\x06\\n\\x02JM\\x10o\\x12\\x06\\n\\x02JP\\x10p\\x12\\x06\\n\\x02JE\\x10q\\x12\\x06\\n\\x02JO\\x10r\\x12\\x06\\n\\x02KZ\\x10s\\x12\\x06\\n\\x02KE\\x10t\\x12\\x06\\n\\x02KI\\x10u\\x12\\x06\\n\\x02KP\\x10v\\x12\\x06\\n\\x02KR\\x10w\\x12\\x06\\n\\x02KW\\x10x\\x12\\x06\\n\\x02KG\\x10y\\x12\\x06\\n\\x02LA\\x10z\\x12\\x06\\n\\x02LV\\x10{\\x12\\x06\\n\\x02LB\\x10|\\x12\\x06\\n\\x02LS\\x10}\\x12\\x06\\n\\x02LR\\x10~\\x12\\x06\\n\\x02LY\\x10\\x7f\\x12\\x07\\n\\x02LI\\x10\\x80\\x01\\x12\\x07\\n\\x02LT\\x10\\x81\\x01\\x12\\x07\\n\\x02LU\\x10\\x82\\x01\\x12\\x07\\n\\x02MO\\x10\\x83\\x01\\x12\\x07\\n\\x02MK\\x10\\x84\\x01\\x12\\x07\\n\\x02MG\\x10\\x85\\x01\\x12\\x07\\n\\x02MW\\x10\\x86\\x01\\x12\\x07\\n\\x02MY\\x10\\x87\\x01\\x12\\x07\\n\\x02MV\\x10\\x88\\x01\\x12\\x07\\n\\x02ML\\x10\\x89\\x01\\x12\\x07\\n\\x02MT\\x10\\x8a\\x01\\x12\\x07\\n\\x02MH\\x10\\x8b\\x01\\x12\\x07\\n\\x02MQ\\x10\\x8c\\x01\\x12\\x07\\n\\x02MR\\x10\\x8d\\x01\\x12\\x07\\n\\x02MU\\x10\\x8e\\x01\\x12\\x07\\n\\x02YT\\x10\\x8f\\x01\\x12\\x07\\n\\x02MX\\x10\\x90\\x01\\x12\\x07\\n\\x02\\x46M\\x10\\x91\\x01\\x12\\x07\\n\\x02MD\\x10\\x92\\x01\\x12\\x07\\n\\x02MC\\x10\\x93\\x01\\x12\\x07\\n\\x02MN\\x10\\x94\\x01\\x12\\x07\\n\\x02ME\\x10\\x95\\x01\\x12\\x07\\n\\x02MS\\x10\\x96\\x01\\x12\\x07\\n\\x02MA\\x10\\x97\\x01\\x12\\x07\\n\\x02MZ\\x10\\x98\\x01\\x12\\x07\\n\\x02MM\\x10\\x99\\x01\\x12\\x07\\n\\x02NA\\x10\\x9a\\x01\\x12\\x07\\n\\x02NR\\x10\\x9b\\x01\\x12\\x07\\n\\x02NP\\x10\\x9c\\x01\\x12\\x07\\n\\x02NL\\x10\\x9d\\x01\\x12\\x07\\n\\x02NC\\x10\\x9e\\x01\\x12\\x07\\n\\x02NZ\\x10\\x9f\\x01\\x12\\x07\\n\\x02NI\\x10\\xa0\\x01\\x12\\x07\\n\\x02NE\\x10\\xa1\\x01\\x12\\x07\\n\\x02NG\\x10\\xa2\\x01\\x12\\x07\\n\\x02NU\\x10\\xa3\\x01\\x12\\x07\\n\\x02NF\\x10\\xa4\\x01\\x12\\x07\\n\\x02MP\\x10\\xa5\\x01\\x12\\x07\\n\\x02NO\\x10\\xa6\\x01\\x12\\x07\\n\\x02OM\\x10\\xa7\\x01\\x12\\x07\\n\\x02PK\\x10\\xa8\\x01\\x12\\x07\\n\\x02PW\\x10\\xa9\\x01\\x12\\x07\\n\\x02PS\\x10\\xaa\\x01\\x12\\x07\\n\\x02PA\\x10\\xab\\x01\\x12\\x07\\n\\x02PG\\x10\\xac\\x01\\x12\\x07\\n\\x02PY\\x10\\xad\\x01\\x12\\x07\\n\\x02PE\\x10\\xae\\x01\\x12\\x07\\n\\x02PH\\x10\\xaf\\x01\\x12\\x07\\n\\x02PN\\x10\\xb0\\x01\\x12\\x07\\n\\x02PL\\x10\\xb1\\x01\\x12\\x07\\n\\x02PT\\x10\\xb2\\x01\\x12\\x07\\n\\x02PR\\x10\\xb3\\x01\\x12\\x07\\n\\x02QA\\x10\\xb4\\x01\\x12\\x07\\n\\x02RE\\x10\\xb5\\x01\\x12\\x07\\n\\x02RO\\x10\\xb6\\x01\\x12\\x07\\n\\x02RU\\x10\\xb7\\x01\\x12\\x07\\n\\x02RW\\x10\\xb8\\x01\\x12\\x07\\n\\x02\\x42L\\x10\\xb9\\x01\\x12\\x07\\n\\x02SH\\x10\\xba\\x01\\x12\\x07\\n\\x02KN\\x10\\xbb\\x01\\x12\\x07\\n\\x02LC\\x10\\xbc\\x01\\x12\\x07\\n\\x02MF\\x10\\xbd\\x01\\x12\\x07\\n\\x02PM\\x10\\xbe\\x01\\x12\\x07\\n\\x02VC\\x10\\xbf\\x01\\x12\\x07\\n\\x02WS\\x10\\xc0\\x01\\x12\\x07\\n\\x02SM\\x10\\xc1\\x01\\x12\\x07\\n\\x02ST\\x10\\xc2\\x01\\x12\\x07\\n\\x02SA\\x10\\xc3\\x01\\x12\\x07\\n\\x02SN\\x10\\xc4\\x01\\x12\\x07\\n\\x02RS\\x10\\xc5\\x01\\x12\\x07\\n\\x02SC\\x10\\xc6\\x01\\x12\\x07\\n\\x02SL\\x10\\xc7\\x01\\x12\\x07\\n\\x02SG\\x10\\xc8\\x01\\x12\\x07\\n\\x02SX\\x10\\xc9\\x01\\x12\\x07\\n\\x02SK\\x10\\xca\\x01\\x12\\x07\\n\\x02SI\\x10\\xcb\\x01\\x12\\x07\\n\\x02SB\\x10\\xcc\\x01\\x12\\x07\\n\\x02SO\\x10\\xcd\\x01\\x12\\x07\\n\\x02ZA\\x10\\xce\\x01\\x12\\x07\\n\\x02GS\\x10\\xcf\\x01\\x12\\x07\\n\\x02SS\\x10\\xd0\\x01\\x12\\x07\\n\\x02\\x45S\\x10\\xd1\\x01\\x12\\x07\\n\\x02LK\\x10\\xd2\\x01\\x12\\x07\\n\\x02SD\\x10\\xd3\\x01\\x12\\x07\\n\\x02SR\\x10\\xd4\\x01\\x12\\x07\\n\\x02SJ\\x10\\xd5\\x01\\x12\\x07\\n\\x02SZ\\x10\\xd6\\x01\\x12\\x07\\n\\x02SE\\x10\\xd7\\x01\\x12\\x07\\n\\x02\\x43H\\x10\\xd8\\x01\\x12\\x07\\n\\x02SY\\x10\\xd9\\x01\\x12\\x07\\n\\x02TW\\x10\\xda\\x01\\x12\\x07\\n\\x02TJ\\x10\\xdb\\x01\\x12\\x07\\n\\x02TZ\\x10\\xdc\\x01\\x12\\x07\\n\\x02TH\\x10\\xdd\\x01\\x12\\x07\\n\\x02TL\\x10\\xde\\x01\\x12\\x07\\n\\x02TG\\x10\\xdf\\x01\\x12\\x07\\n\\x02TK\\x10\\xe0\\x01\\x12\\x07\\n\\x02TO\\x10\\xe1\\x01\\x12\\x07\\n\\x02TT\\x10\\xe2\\x01\\x12\\x07\\n\\x02TN\\x10\\xe3\\x01\\x12\\x07\\n\\x02TR\\x10\\xe4\\x01\\x12\\x07\\n\\x02TM\\x10\\xe5\\x01\\x12\\x07\\n\\x02TC\\x10\\xe6\\x01\\x12\\x07\\n\\x02TV\\x10\\xe7\\x01\\x12\\x07\\n\\x02UG\\x10\\xe8\\x01\\x12\\x07\\n\\x02UA\\x10\\xe9\\x01\\x12\\x07\\n\\x02\\x41\\x45\\x10\\xea\\x01\\x12\\x07\\n\\x02GB\\x10\\xeb\\x01\\x12\\x07\\n\\x02US\\x10\\xec\\x01\\x12\\x07\\n\\x02UM\\x10\\xed\\x01\\x12\\x07\\n\\x02UY\\x10\\xee\\x01\\x12\\x07\\n\\x02UZ\\x10\\xef\\x01\\x12\\x07\\n\\x02VU\\x10\\xf0\\x01\\x12\\x07\\n\\x02VE\\x10\\xf1\\x01\\x12\\x07\\n\\x02VN\\x10\\xf2\\x01\\x12\\x07\\n\\x02VG\\x10\\xf3\\x01\\x12\\x07\\n\\x02VI\\x10\\xf4\\x01\\x12\\x07\\n\\x02WF\\x10\\xf5\\x01\\x12\\x07\\n\\x02\\x45H\\x10\\xf6\\x01\\x12\\x07\\n\\x02YE\\x10\\xf7\\x01\\x12\\x07\\n\\x02ZM\\x10\\xf8\\x01\\x12\\x07\\n\\x02ZW\\x10\\xf9\\x01\\x12\\t\\n\\x04R001\\x10\\xfa\\x01\\x12\\t\\n\\x04R002\\x10\\xfb\\x01\\x12\\t\\n\\x04R015\\x10\\xfc\\x01\\x12\\t\\n\\x04R012\\x10\\xfd\\x01\\x12\\t\\n\\x04R818\\x10\\xfe\\x01\\x12\\t\\n\\x04R434\\x10\\xff\\x01\\x12\\t\\n\\x04R504\\x10\\x80\\x02\\x12\\t\\n\\x04R729\\x10\\x81\\x02\\x12\\t\\n\\x04R788\\x10\\x82\\x02\\x12\\t\\n\\x04R732\\x10\\x83\\x02\\x12\\t\\n\\x04R202\\x10\\x84\\x02\\x12\\t\\n\\x04R014\\x10\\x85\\x02\\x12\\t\\n\\x04R086\\x10\\x86\\x02\\x12\\t\\n\\x04R108\\x10\\x87\\x02\\x12\\t\\n\\x04R174\\x10\\x88\\x02\\x12\\t\\n\\x04R262\\x10\\x89\\x02\\x12\\t\\n\\x04R232\\x10\\x8a\\x02\\x12\\t\\n\\x04R231\\x10\\x8b\\x02\\x12\\t\\n\\x04R260\\x10\\x8c\\x02\\x12\\t\\n\\x04R404\\x10\\x8d\\x02\\x12\\t\\n\\x04R450\\x10\\x8e\\x02\\x12\\t\\n\\x04R454\\x10\\x8f\\x02\\x12\\t\\n\\x04R480\\x10\\x90\\x02\\x12\\t\\n\\x04R175\\x10\\x91\\x02\\x12\\t\\n\\x04R508\\x10\\x92\\x02\\x12\\t\\n\\x04R638\\x10\\x93\\x02\\x12\\t\\n\\x04R646\\x10\\x94\\x02\\x12\\t\\n\\x04R690\\x10\\x95\\x02\\x12\\t\\n\\x04R706\\x10\\x96\\x02\\x12\\t\\n\\x04R728\\x10\\x97\\x02\\x12\\t\\n\\x04R800\\x10\\x98\\x02\\x12\\t\\n\\x04R834\\x10\\x99\\x02\\x12\\t\\n\\x04R894\\x10\\x9a\\x02\\x12\\t\\n\\x04R716\\x10\\x9b\\x02\\x12\\t\\n\\x04R017\\x10\\x9c\\x02\\x12\\t\\n\\x04R024\\x10\\x9d\\x02\\x12\\t\\n\\x04R120\\x10\\x9e\\x02\\x12\\t\\n\\x04R140\\x10\\x9f\\x02\\x12\\t\\n\\x04R148\\x10\\xa0\\x02\\x12\\t\\n\\x04R178\\x10\\xa1\\x02\\x12\\t\\n\\x04R180\\x10\\xa2\\x02\\x12\\t\\n\\x04R226\\x10\\xa3\\x02\\x12\\t\\n\\x04R266\\x10\\xa4\\x02\\x12\\t\\n\\x04R678\\x10\\xa5\\x02\\x12\\t\\n\\x04R018\\x10\\xa6\\x02\\x12\\t\\n\\x04R072\\x10\\xa7\\x02\\x12\\t\\n\\x04R748\\x10\\xa8\\x02\\x12\\t\\n\\x04R426\\x10\\xa9\\x02\\x12\\t\\n\\x04R516\\x10\\xaa\\x02\\x12\\t\\n\\x04R710\\x10\\xab\\x02\\x12\\t\\n\\x04R011\\x10\\xac\\x02\\x12\\t\\n\\x04R204\\x10\\xad\\x02\\x12\\t\\n\\x04R854\\x10\\xae\\x02\\x12\\t\\n\\x04R132\\x10\\xaf\\x02\\x12\\t\\n\\x04R384\\x10\\xb0\\x02\\x12\\t\\n\\x04R270\\x10\\xb1\\x02\\x12\\t\\n\\x04R288\\x10\\xb2\\x02\\x12\\t\\n\\x04R324\\x10\\xb3\\x02\\x12\\t\\n\\x04R624\\x10\\xb4\\x02\\x12\\t\\n\\x04R430\\x10\\xb5\\x02\\x12\\t\\n\\x04R466\\x10\\xb6\\x02\\x12\\t\\n\\x04R478\\x10\\xb7\\x02\\x12\\t\\n\\x04R562\\x10\\xb8\\x02\\x12\\t\\n\\x04R566\\x10\\xb9\\x02\\x12\\t\\n\\x04R654\\x10\\xba\\x02\\x12\\t\\n\\x04R686\\x10\\xbb\\x02\\x12\\t\\n\\x04R694\\x10\\xbc\\x02\\x12\\t\\n\\x04R768\\x10\\xbd\\x02\\x12\\t\\n\\x04R019\\x10\\xbe\\x02\\x12\\t\\n\\x04R419\\x10\\xbf\\x02\\x12\\t\\n\\x04R029\\x10\\xc0\\x02\\x12\\t\\n\\x04R660\\x10\\xc1\\x02\\x12\\t\\n\\x04R028\\x10\\xc2\\x02\\x12\\t\\n\\x04R533\\x10\\xc3\\x02\\x12\\t\\n\\x04R044\\x10\\xc4\\x02\\x12\\t\\n\\x04R052\\x10\\xc5\\x02\\x12\\t\\n\\x04R535\\x10\\xc6\\x02\\x12\\t\\n\\x04R092\\x10\\xc7\\x02\\x12\\t\\n\\x04R136\\x10\\xc8\\x02\\x12\\t\\n\\x04R192\\x10\\xc9\\x02\\x12\\t\\n\\x04R531\\x10\\xca\\x02\\x12\\t\\n\\x04R212\\x10\\xcb\\x02\\x12\\t\\n\\x04R214\\x10\\xcc\\x02\\x12\\t\\n\\x04R308\\x10\\xcd\\x02\\x12\\t\\n\\x04R312\\x10\\xce\\x02\\x12\\t\\n\\x04R332\\x10\\xcf\\x02\\x12\\t\\n\\x04R388\\x10\\xd0\\x02\\x12\\t\\n\\x04R474\\x10\\xd1\\x02\\x12\\t\\n\\x04R500\\x10\\xd2\\x02\\x12\\t\\n\\x04R630\\x10\\xd3\\x02\\x12\\t\\n\\x04R652\\x10\\xd4\\x02\\x12\\t\\n\\x04R659\\x10\\xd5\\x02\\x12\\t\\n\\x04R662\\x10\\xd6\\x02\\x12\\t\\n\\x04R663\\x10\\xd7\\x02\\x12\\t\\n\\x04R670\\x10\\xd8\\x02\\x12\\t\\n\\x04R534\\x10\\xd9\\x02\\x12\\t\\n\\x04R780\\x10\\xda\\x02\\x12\\t\\n\\x04R796\\x10\\xdb\\x02\\x12\\t\\n\\x04R850\\x10\\xdc\\x02\\x12\\t\\n\\x04R013\\x10\\xdd\\x02\\x12\\t\\n\\x04R084\\x10\\xde\\x02\\x12\\t\\n\\x04R188\\x10\\xdf\\x02\\x12\\t\\n\\x04R222\\x10\\xe0\\x02\\x12\\t\\n\\x04R320\\x10\\xe1\\x02\\x12\\t\\n\\x04R340\\x10\\xe2\\x02\\x12\\t\\n\\x04R484\\x10\\xe3\\x02\\x12\\t\\n\\x04R558\\x10\\xe4\\x02\\x12\\t\\n\\x04R591\\x10\\xe5\\x02\\x12\\t\\n\\x04R005\\x10\\xe6\\x02\\x12\\t\\n\\x04R032\\x10\\xe7\\x02\\x12\\t\\n\\x04R068\\x10\\xe8\\x02\\x12\\t\\n\\x04R074\\x10\\xe9\\x02\\x12\\t\\n\\x04R076\\x10\\xea\\x02\\x12\\t\\n\\x04R152\\x10\\xeb\\x02\\x12\\t\\n\\x04R170\\x10\\xec\\x02\\x12\\t\\n\\x04R218\\x10\\xed\\x02\\x12\\t\\n\\x04R238\\x10\\xee\\x02\\x12\\t\\n\\x04R254\\x10\\xef\\x02\\x12\\t\\n\\x04R328\\x10\\xf0\\x02\\x12\\t\\n\\x04R600\\x10\\xf1\\x02\\x12\\t\\n\\x04R604\\x10\\xf2\\x02\\x12\\t\\n\\x04R239\\x10\\xf3\\x02\\x12\\t\\n\\x04R740\\x10\\xf4\\x02\\x12\\t\\n\\x04R858\\x10\\xf5\\x02\\x12\\t\\n\\x04R862\\x10\\xf6\\x02\\x12\\t\\n\\x04R021\\x10\\xf7\\x02\\x12\\t\\n\\x04R060\\x10\\xf8\\x02\\x12\\t\\n\\x04R124\\x10\\xf9\\x02\\x12\\t\\n\\x04R304\\x10\\xfa\\x02\\x12\\t\\n\\x04R666\\x10\\xfb\\x02\\x12\\t\\n\\x04R840\\x10\\xfc\\x02\\x12\\t\\n\\x04R010\\x10\\xfd\\x02\\x12\\t\\n\\x04R142\\x10\\xfe\\x02\\x12\\t\\n\\x04R143\\x10\\xff\\x02\\x12\\t\\n\\x04R398\\x10\\x80\\x03\\x12\\t\\n\\x04R417\\x10\\x81\\x03\\x12\\t\\n\\x04R762\\x10\\x82\\x03\\x12\\t\\n\\x04R795\\x10\\x83\\x03\\x12\\t\\n\\x04R860\\x10\\x84\\x03\\x12\\t\\n\\x04R030\\x10\\x85\\x03\\x12\\t\\n\\x04R156\\x10\\x86\\x03\\x12\\t\\n\\x04R344\\x10\\x87\\x03\\x12\\t\\n\\x04R446\\x10\\x88\\x03\\x12\\t\\n\\x04R408\\x10\\x89\\x03\\x12\\t\\n\\x04R392\\x10\\x8a\\x03\\x12\\t\\n\\x04R496\\x10\\x8b\\x03\\x12\\t\\n\\x04R410\\x10\\x8c\\x03\\x12\\t\\n\\x04R035\\x10\\x8d\\x03\\x12\\t\\n\\x04R096\\x10\\x8e\\x03\\x12\\t\\n\\x04R116\\x10\\x8f\\x03\\x12\\t\\n\\x04R360\\x10\\x90\\x03\\x12\\t\\n\\x04R418\\x10\\x91\\x03\\x12\\t\\n\\x04R458\\x10\\x92\\x03\\x12\\t\\n\\x04R104\\x10\\x93\\x03\\x12\\t\\n\\x04R608\\x10\\x94\\x03\\x12\\t\\n\\x04R702\\x10\\x95\\x03\\x12\\t\\n\\x04R764\\x10\\x96\\x03\\x12\\t\\n\\x04R626\\x10\\x97\\x03\\x12\\t\\n\\x04R704\\x10\\x98\\x03\\x12\\t\\n\\x04R034\\x10\\x99\\x03\\x12\\t\\n\\x04R004\\x10\\x9a\\x03\\x12\\t\\n\\x04R050\\x10\\x9b\\x03\\x12\\t\\n\\x04R064\\x10\\x9c\\x03\\x12\\t\\n\\x04R356\\x10\\x9d\\x03\\x12\\t\\n\\x04R364\\x10\\x9e\\x03\\x12\\t\\n\\x04R462\\x10\\x9f\\x03\\x12\\t\\n\\x04R524\\x10\\xa0\\x03\\x12\\t\\n\\x04R586\\x10\\xa1\\x03\\x12\\t\\n\\x04R144\\x10\\xa2\\x03\\x12\\t\\n\\x04R145\\x10\\xa3\\x03\\x12\\t\\n\\x04R051\\x10\\xa4\\x03\\x12\\t\\n\\x04R031\\x10\\xa5\\x03\\x12\\t\\n\\x04R048\\x10\\xa6\\x03\\x12\\t\\n\\x04R196\\x10\\xa7\\x03\\x12\\t\\n\\x04R268\\x10\\xa8\\x03\\x12\\t\\n\\x04R368\\x10\\xa9\\x03\\x12\\t\\n\\x04R376\\x10\\xaa\\x03\\x12\\t\\n\\x04R400\\x10\\xab\\x03\\x12\\t\\n\\x04R414\\x10\\xac\\x03\\x12\\t\\n\\x04R422\\x10\\xad\\x03\\x12\\t\\n\\x04R512\\x10\\xae\\x03\\x12\\t\\n\\x04R634\\x10\\xaf\\x03\\x12\\t\\n\\x04R682\\x10\\xb0\\x03\\x12\\t\\n\\x04R275\\x10\\xb1\\x03\\x12\\t\\n\\x04R760\\x10\\xb2\\x03\\x12\\t\\n\\x04R792\\x10\\xb3\\x03\\x12\\t\\n\\x04R784\\x10\\xb4\\x03\\x12\\t\\n\\x04R887\\x10\\xb5\\x03\\x12\\t\\n\\x04R150\\x10\\xb6\\x03\\x12\\t\\n\\x04R151\\x10\\xb7\\x03\\x12\\t\\n\\x04R112\\x10\\xb8\\x03\\x12\\t\\n\\x04R100\\x10\\xb9\\x03\\x12\\t\\n\\x04R203\\x10\\xba\\x03\\x12\\t\\n\\x04R348\\x10\\xbb\\x03\\x12\\t\\n\\x04R616\\x10\\xbc\\x03\\x12\\t\\n\\x04R498\\x10\\xbd\\x03\\x12\\t\\n\\x04R642\\x10\\xbe\\x03\\x12\\t\\n\\x04R643\\x10\\xbf\\x03\\x12\\t\\n\\x04R703\\x10\\xc0\\x03\\x12\\t\\n\\x04R804\\x10\\xc1\\x03\\x12\\t\\n\\x04R154\\x10\\xc2\\x03\\x12\\t\\n\\x04R248\\x10\\xc3\\x03\\x12\\t\\n\\x04R830\\x10\\xc4\\x03\\x12\\t\\n\\x04R831\\x10\\xc5\\x03\\x12\\t\\n\\x04R832\\x10\\xc6\\x03\\x12\\t\\n\\x04R680\\x10\\xc7\\x03\\x12\\t\\n\\x04R208\\x10\\xc8\\x03\\x12\\t\\n\\x04R233\\x10\\xc9\\x03\\x12\\t\\n\\x04R234\\x10\\xca\\x03\\x12\\t\\n\\x04R246\\x10\\xcb\\x03\\x12\\t\\n\\x04R352\\x10\\xcc\\x03\\x12\\t\\n\\x04R372\\x10\\xcd\\x03\\x12\\t\\n\\x04R833\\x10\\xce\\x03\\x12\\t\\n\\x04R428\\x10\\xcf\\x03\\x12\\t\\n\\x04R440\\x10\\xd0\\x03\\x12\\t\\n\\x04R578\\x10\\xd1\\x03\\x12\\t\\n\\x04R744\\x10\\xd2\\x03\\x12\\t\\n\\x04R752\\x10\\xd3\\x03\\x12\\t\\n\\x04R826\\x10\\xd4\\x03\\x12\\t\\n\\x04R039\\x10\\xd5\\x03\\x12\\t\\n\\x04R008\\x10\\xd6\\x03\\x12\\t\\n\\x04R020\\x10\\xd7\\x03\\x12\\t\\n\\x04R070\\x10\\xd8\\x03\\x12\\t\\n\\x04R191\\x10\\xd9\\x03\\x12\\t\\n\\x04R292\\x10\\xda\\x03\\x12\\t\\n\\x04R300\\x10\\xdb\\x03\\x12\\t\\n\\x04R336\\x10\\xdc\\x03\\x12\\t\\n\\x04R380\\x10\\xdd\\x03\\x12\\t\\n\\x04R470\\x10\\xde\\x03\\x12\\t\\n\\x04R499\\x10\\xdf\\x03\\x12\\t\\n\\x04R807\\x10\\xe0\\x03\\x12\\t\\n\\x04R620\\x10\\xe1\\x03\\x12\\t\\n\\x04R674\\x10\\xe2\\x03\\x12\\t\\n\\x04R688\\x10\\xe3\\x03\\x12\\t\\n\\x04R705\\x10\\xe4\\x03\\x12\\t\\n\\x04R724\\x10\\xe5\\x03\\x12\\t\\n\\x04R155\\x10\\xe6\\x03\\x12\\t\\n\\x04R040\\x10\\xe7\\x03\\x12\\t\\n\\x04R056\\x10\\xe8\\x03\\x12\\t\\n\\x04R250\\x10\\xe9\\x03\\x12\\t\\n\\x04R276\\x10\\xea\\x03\\x12\\t\\n\\x04R438\\x10\\xeb\\x03\\x12\\t\\n\\x04R442\\x10\\xec\\x03\\x12\\t\\n\\x04R492\\x10\\xed\\x03\\x12\\t\\n\\x04R528\\x10\\xee\\x03\\x12\\t\\n\\x04R756\\x10\\xef\\x03\\x12\\t\\n\\x04R009\\x10\\xf0\\x03\\x12\\t\\n\\x04R053\\x10\\xf1\\x03\\x12\\t\\n\\x04R036\\x10\\xf2\\x03\\x12\\t\\n\\x04R162\\x10\\xf3\\x03\\x12\\t\\n\\x04R166\\x10\\xf4\\x03\\x12\\t\\n\\x04R334\\x10\\xf5\\x03\\x12\\t\\n\\x04R554\\x10\\xf6\\x03\\x12\\t\\n\\x04R574\\x10\\xf7\\x03\\x12\\t\\n\\x04R054\\x10\\xf8\\x03\\x12\\t\\n\\x04R242\\x10\\xf9\\x03\\x12\\t\\n\\x04R540\\x10\\xfa\\x03\\x12\\t\\n\\x04R598\\x10\\xfb\\x03\\x12\\t\\n\\x04R090\\x10\\xfc\\x03\\x12\\t\\n\\x04R548\\x10\\xfd\\x03\\x12\\t\\n\\x04R057\\x10\\xfe\\x03\\x12\\t\\n\\x04R316\\x10\\xff\\x03\\x12\\t\\n\\x04R296\\x10\\x80\\x04\\x12\\t\\n\\x04R584\\x10\\x81\\x04\\x12\\t\\n\\x04R583\\x10\\x82\\x04\\x12\\t\\n\\x04R520\\x10\\x83\\x04\\x12\\t\\n\\x04R580\\x10\\x84\\x04\\x12\\t\\n\\x04R585\\x10\\x85\\x04\\x12\\t\\n\\x04R581\\x10\\x86\\x04\\x12\\t\\n\\x04R061\\x10\\x87\\x04\\x12\\t\\n\\x04R016\\x10\\x88\\x04\\x12\\t\\n\\x04R184\\x10\\x89\\x04\\x12\\t\\n\\x04R258\\x10\\x8a\\x04\\x12\\t\\n\\x04R570\\x10\\x8b\\x04\\x12\\t\\n\\x04R612\\x10\\x8c\\x04\\x12\\t\\n\\x04R882\\x10\\x8d\\x04\\x12\\t\\n\\x04R772\\x10\\x8e\\x04\\x12\\t\\n\\x04R776\\x10\\x8f\\x04\\x12\\t\\n\\x04R798\\x10\\x90\\x04\\x12\\t\\n\\x04R876\\x10\\x91\\x04\\x62\\x06proto3')\n)\n_sym_db.RegisterFileDescriptor(DESCRIPTOR)\n\n\n\n_CLAIMLIST_LISTTYPE = _descriptor.EnumDescriptor(\n  name='ListType',\n  full_name='pb.ClaimList.ListType',\n  filename=None,\n  file=DESCRIPTOR,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='COLLECTION', index=0, number=0,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='DERIVATION', index=1, number=2,\n      options=None,\n      type=None),\n  ],\n  containing_type=None,\n  options=None,\n  serialized_start=852,\n  serialized_end=894,\n)\n_sym_db.RegisterEnumDescriptor(_CLAIMLIST_LISTTYPE)\n\n_FEE_CURRENCY = _descriptor.EnumDescriptor(\n  name='Currency',\n  full_name='pb.Fee.Currency',\n  filename=None,\n  file=DESCRIPTOR,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='UNKNOWN_CURRENCY', index=0, number=0,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='LBC', index=1, number=1,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BTC', index=2, number=2,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='USD', index=3, number=3,\n      options=None,\n      type=None),\n  ],\n  containing_type=None,\n  options=None,\n  serialized_start=1096,\n  serialized_end=1155,\n)\n_sym_db.RegisterEnumDescriptor(_FEE_CURRENCY)\n\n_SOFTWARE_OS = _descriptor.EnumDescriptor(\n  name='OS',\n  full_name='pb.Software.OS',\n  filename=None,\n  file=DESCRIPTOR,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='UNKNOWN_OS', index=0, number=0,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ANY', index=1, number=1,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='LINUX', index=2, number=2,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='WINDOWS', index=3, number=3,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MAC', index=4, number=4,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ANDROID', index=5, number=5,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='IOS', index=6, number=6,\n      options=None,\n      type=None),\n  ],\n  containing_type=None,\n  options=None,\n  serialized_start=1332,\n  serialized_end=1416,\n)\n_sym_db.RegisterEnumDescriptor(_SOFTWARE_OS)\n\n_LANGUAGE_LANGUAGE = _descriptor.EnumDescriptor(\n  name='Language',\n  full_name='pb.Language.Language',\n  filename=None,\n  file=DESCRIPTOR,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='UNKNOWN_LANGUAGE', index=0, number=0,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='en', index=1, number=1,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='aa', index=2, number=2,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ab', index=3, number=3,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ae', index=4, number=4,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='af', index=5, number=5,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ak', index=6, number=6,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='am', index=7, number=7,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='an', index=8, number=8,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ar', index=9, number=9,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='as', index=10, number=10,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='av', index=11, number=11,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ay', index=12, number=12,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='az', index=13, number=13,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ba', index=14, number=14,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='be', index=15, number=15,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='bg', index=16, number=16,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='bh', index=17, number=17,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='bi', index=18, number=18,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='bm', index=19, number=19,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='bn', index=20, number=20,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='bo', index=21, number=21,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='br', index=22, number=22,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='bs', index=23, number=23,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ca', index=24, number=24,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ce', index=25, number=25,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ch', index=26, number=26,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='co', index=27, number=27,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='cr', index=28, number=28,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='cs', index=29, number=29,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='cu', index=30, number=30,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='cv', index=31, number=31,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='cy', index=32, number=32,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='da', index=33, number=33,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='de', index=34, number=34,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='dv', index=35, number=35,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='dz', index=36, number=36,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ee', index=37, number=37,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='el', index=38, number=38,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='eo', index=39, number=39,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='es', index=40, number=40,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='et', index=41, number=41,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='eu', index=42, number=42,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='fa', index=43, number=43,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ff', index=44, number=44,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='fi', index=45, number=45,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='fj', index=46, number=46,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='fo', index=47, number=47,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='fr', index=48, number=48,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='fy', index=49, number=49,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ga', index=50, number=50,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='gd', index=51, number=51,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='gl', index=52, number=52,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='gn', index=53, number=53,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='gu', index=54, number=54,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='gv', index=55, number=55,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ha', index=56, number=56,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='he', index=57, number=57,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='hi', index=58, number=58,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ho', index=59, number=59,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='hr', index=60, number=60,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ht', index=61, number=61,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='hu', index=62, number=62,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='hy', index=63, number=63,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='hz', index=64, number=64,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ia', index=65, number=65,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='id', index=66, number=66,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ie', index=67, number=67,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ig', index=68, number=68,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ii', index=69, number=69,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ik', index=70, number=70,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='io', index=71, number=71,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='is', index=72, number=72,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='it', index=73, number=73,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='iu', index=74, number=74,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ja', index=75, number=75,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='jv', index=76, number=76,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ka', index=77, number=77,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='kg', index=78, number=78,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ki', index=79, number=79,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='kj', index=80, number=80,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='kk', index=81, number=81,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='kl', index=82, number=82,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='km', index=83, number=83,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='kn', index=84, number=84,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ko', index=85, number=85,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='kr', index=86, number=86,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ks', index=87, number=87,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ku', index=88, number=88,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='kv', index=89, number=89,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='kw', index=90, number=90,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ky', index=91, number=91,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='la', index=92, number=92,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='lb', index=93, number=93,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='lg', index=94, number=94,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='li', index=95, number=95,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ln', index=96, number=96,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='lo', index=97, number=97,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='lt', index=98, number=98,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='lu', index=99, number=99,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='lv', index=100, number=100,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='mg', index=101, number=101,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='mh', index=102, number=102,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='mi', index=103, number=103,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='mk', index=104, number=104,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ml', index=105, number=105,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='mn', index=106, number=106,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='mr', index=107, number=107,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ms', index=108, number=108,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='mt', index=109, number=109,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='my', index=110, number=110,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='na', index=111, number=111,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='nb', index=112, number=112,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='nd', index=113, number=113,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ne', index=114, number=114,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ng', index=115, number=115,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='nl', index=116, number=116,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='nn', index=117, number=117,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='no', index=118, number=118,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='nr', index=119, number=119,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='nv', index=120, number=120,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ny', index=121, number=121,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='oc', index=122, number=122,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='oj', index=123, number=123,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='om', index=124, number=124,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='or', index=125, number=125,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='os', index=126, number=126,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='pa', index=127, number=127,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='pi', index=128, number=128,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='pl', index=129, number=129,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ps', index=130, number=130,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='pt', index=131, number=131,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='qu', index=132, number=132,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='rm', index=133, number=133,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='rn', index=134, number=134,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ro', index=135, number=135,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ru', index=136, number=136,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='rw', index=137, number=137,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sa', index=138, number=138,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sc', index=139, number=139,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sd', index=140, number=140,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='se', index=141, number=141,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sg', index=142, number=142,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='si', index=143, number=143,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sk', index=144, number=144,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sl', index=145, number=145,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sm', index=146, number=146,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sn', index=147, number=147,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='so', index=148, number=148,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sq', index=149, number=149,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sr', index=150, number=150,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ss', index=151, number=151,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='st', index=152, number=152,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='su', index=153, number=153,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sv', index=154, number=154,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='sw', index=155, number=155,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ta', index=156, number=156,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='te', index=157, number=157,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='tg', index=158, number=158,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='th', index=159, number=159,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ti', index=160, number=160,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='tk', index=161, number=161,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='tl', index=162, number=162,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='tn', index=163, number=163,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='to', index=164, number=164,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='tr', index=165, number=165,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ts', index=166, number=166,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='tt', index=167, number=167,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='tw', index=168, number=168,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ty', index=169, number=169,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ug', index=170, number=170,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='uk', index=171, number=171,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ur', index=172, number=172,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='uz', index=173, number=173,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ve', index=174, number=174,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='vi', index=175, number=175,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='vo', index=176, number=176,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='wa', index=177, number=177,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='wo', index=178, number=178,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='xh', index=179, number=179,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='yi', index=180, number=180,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='yo', index=181, number=181,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='za', index=182, number=182,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='zh', index=183, number=183,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='zu', index=184, number=184,\n      options=None,\n      type=None),\n  ],\n  containing_type=None,\n  options=None,\n  serialized_start=1548,\n  serialized_end=3109,\n)\n_sym_db.RegisterEnumDescriptor(_LANGUAGE_LANGUAGE)\n\n_LANGUAGE_SCRIPT = _descriptor.EnumDescriptor(\n  name='Script',\n  full_name='pb.Language.Script',\n  filename=None,\n  file=DESCRIPTOR,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='UNKNOWN_SCRIPT', index=0, number=0,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Adlm', index=1, number=1,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Afak', index=2, number=2,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Aghb', index=3, number=3,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Ahom', index=4, number=4,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Arab', index=5, number=5,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Aran', index=6, number=6,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Armi', index=7, number=7,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Armn', index=8, number=8,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Avst', index=9, number=9,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Bali', index=10, number=10,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Bamu', index=11, number=11,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Bass', index=12, number=12,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Batk', index=13, number=13,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Beng', index=14, number=14,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Bhks', index=15, number=15,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Blis', index=16, number=16,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Bopo', index=17, number=17,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Brah', index=18, number=18,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Brai', index=19, number=19,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Bugi', index=20, number=20,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Buhd', index=21, number=21,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Cakm', index=22, number=22,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Cans', index=23, number=23,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Cari', index=24, number=24,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Cham', index=25, number=25,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Cher', index=26, number=26,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Cirt', index=27, number=27,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Copt', index=28, number=28,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Cpmn', index=29, number=29,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Cprt', index=30, number=30,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Cyrl', index=31, number=31,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Cyrs', index=32, number=32,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Deva', index=33, number=33,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Dogr', index=34, number=34,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Dsrt', index=35, number=35,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Dupl', index=36, number=36,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Egyd', index=37, number=37,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Egyh', index=38, number=38,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Egyp', index=39, number=39,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Elba', index=40, number=40,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Elym', index=41, number=41,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Ethi', index=42, number=42,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Geok', index=43, number=43,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Geor', index=44, number=44,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Glag', index=45, number=45,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Gong', index=46, number=46,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Gonm', index=47, number=47,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Goth', index=48, number=48,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Gran', index=49, number=49,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Grek', index=50, number=50,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Gujr', index=51, number=51,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Guru', index=52, number=52,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Hanb', index=53, number=53,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Hang', index=54, number=54,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Hani', index=55, number=55,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Hano', index=56, number=56,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Hans', index=57, number=57,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Hant', index=58, number=58,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Hatr', index=59, number=59,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Hebr', index=60, number=60,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Hira', index=61, number=61,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Hluw', index=62, number=62,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Hmng', index=63, number=63,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Hmnp', index=64, number=64,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Hrkt', index=65, number=65,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Hung', index=66, number=66,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Inds', index=67, number=67,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Ital', index=68, number=68,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Jamo', index=69, number=69,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Java', index=70, number=70,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Jpan', index=71, number=71,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Jurc', index=72, number=72,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Kali', index=73, number=73,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Kana', index=74, number=74,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Khar', index=75, number=75,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Khmr', index=76, number=76,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Khoj', index=77, number=77,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Kitl', index=78, number=78,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Kits', index=79, number=79,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Knda', index=80, number=80,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Kore', index=81, number=81,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Kpel', index=82, number=82,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Kthi', index=83, number=83,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Lana', index=84, number=84,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Laoo', index=85, number=85,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Latf', index=86, number=86,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Latg', index=87, number=87,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Latn', index=88, number=88,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Leke', index=89, number=89,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Lepc', index=90, number=90,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Limb', index=91, number=91,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Lina', index=92, number=92,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Linb', index=93, number=93,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Lisu', index=94, number=94,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Loma', index=95, number=95,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Lyci', index=96, number=96,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Lydi', index=97, number=97,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Mahj', index=98, number=98,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Maka', index=99, number=99,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Mand', index=100, number=100,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Mani', index=101, number=101,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Marc', index=102, number=102,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Maya', index=103, number=103,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Medf', index=104, number=104,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Mend', index=105, number=105,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Merc', index=106, number=106,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Mero', index=107, number=107,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Mlym', index=108, number=108,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Modi', index=109, number=109,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Mong', index=110, number=110,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Moon', index=111, number=111,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Mroo', index=112, number=112,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Mtei', index=113, number=113,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Mult', index=114, number=114,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Mymr', index=115, number=115,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Nand', index=116, number=116,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Narb', index=117, number=117,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Nbat', index=118, number=118,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Newa', index=119, number=119,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Nkdb', index=120, number=120,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Nkgb', index=121, number=121,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Nkoo', index=122, number=122,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Nshu', index=123, number=123,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Ogam', index=124, number=124,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Olck', index=125, number=125,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Orkh', index=126, number=126,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Orya', index=127, number=127,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Osge', index=128, number=128,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Osma', index=129, number=129,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Palm', index=130, number=130,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Pauc', index=131, number=131,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Perm', index=132, number=132,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Phag', index=133, number=133,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Phli', index=134, number=134,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Phlp', index=135, number=135,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Phlv', index=136, number=136,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Phnx', index=137, number=137,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Plrd', index=138, number=138,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Piqd', index=139, number=139,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Prti', index=140, number=140,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Qaaa', index=141, number=141,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Qabx', index=142, number=142,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Rjng', index=143, number=143,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Rohg', index=144, number=144,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Roro', index=145, number=145,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Runr', index=146, number=146,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Samr', index=147, number=147,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Sara', index=148, number=148,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Sarb', index=149, number=149,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Saur', index=150, number=150,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Sgnw', index=151, number=151,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Shaw', index=152, number=152,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Shrd', index=153, number=153,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Shui', index=154, number=154,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Sidd', index=155, number=155,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Sind', index=156, number=156,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Sinh', index=157, number=157,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Sogd', index=158, number=158,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Sogo', index=159, number=159,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Sora', index=160, number=160,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Soyo', index=161, number=161,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Sund', index=162, number=162,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Sylo', index=163, number=163,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Syrc', index=164, number=164,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Syre', index=165, number=165,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Syrj', index=166, number=166,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Syrn', index=167, number=167,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Tagb', index=168, number=168,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Takr', index=169, number=169,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Tale', index=170, number=170,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Talu', index=171, number=171,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Taml', index=172, number=172,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Tang', index=173, number=173,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Tavt', index=174, number=174,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Telu', index=175, number=175,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Teng', index=176, number=176,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Tfng', index=177, number=177,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Tglg', index=178, number=178,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Thaa', index=179, number=179,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Thai', index=180, number=180,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Tibt', index=181, number=181,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Tirh', index=182, number=182,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Ugar', index=183, number=183,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Vaii', index=184, number=184,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Visp', index=185, number=185,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Wara', index=186, number=186,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Wcho', index=187, number=187,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Wole', index=188, number=188,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Xpeo', index=189, number=189,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Xsux', index=190, number=190,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Yiii', index=191, number=191,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Zanb', index=192, number=192,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Zinh', index=193, number=193,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Zmth', index=194, number=194,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Zsye', index=195, number=195,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Zsym', index=196, number=196,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Zxxx', index=197, number=197,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Zyyy', index=198, number=198,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='Zzzz', index=199, number=199,\n      options=None,\n      type=None),\n  ],\n  containing_type=None,\n  options=None,\n  serialized_start=3112,\n  serialized_end=5202,\n)\n_sym_db.RegisterEnumDescriptor(_LANGUAGE_SCRIPT)\n\n_LOCATION_COUNTRY = _descriptor.EnumDescriptor(\n  name='Country',\n  full_name='pb.Location.Country',\n  filename=None,\n  file=DESCRIPTOR,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='UNKNOWN_COUNTRY', index=0, number=0,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='AF', index=1, number=1,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='AX', index=2, number=2,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='AL', index=3, number=3,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='DZ', index=4, number=4,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='AS', index=5, number=5,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='AD', index=6, number=6,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='AO', index=7, number=7,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='AI', index=8, number=8,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='AQ', index=9, number=9,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='AG', index=10, number=10,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='AR', index=11, number=11,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='AM', index=12, number=12,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='AW', index=13, number=13,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='AU', index=14, number=14,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='AT', index=15, number=15,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='AZ', index=16, number=16,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BS', index=17, number=17,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BH', index=18, number=18,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BD', index=19, number=19,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BB', index=20, number=20,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BY', index=21, number=21,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BE', index=22, number=22,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BZ', index=23, number=23,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BJ', index=24, number=24,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BM', index=25, number=25,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BT', index=26, number=26,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BO', index=27, number=27,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BQ', index=28, number=28,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BA', index=29, number=29,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BW', index=30, number=30,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BV', index=31, number=31,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BR', index=32, number=32,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='IO', index=33, number=33,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BN', index=34, number=34,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BG', index=35, number=35,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BF', index=36, number=36,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BI', index=37, number=37,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='KH', index=38, number=38,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='CM', index=39, number=39,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='CA', index=40, number=40,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='CV', index=41, number=41,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='KY', index=42, number=42,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='CF', index=43, number=43,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='TD', index=44, number=44,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='CL', index=45, number=45,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='CN', index=46, number=46,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='CX', index=47, number=47,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='CC', index=48, number=48,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='CO', index=49, number=49,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='KM', index=50, number=50,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='CG', index=51, number=51,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='CD', index=52, number=52,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='CK', index=53, number=53,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='CR', index=54, number=54,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='CI', index=55, number=55,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='HR', index=56, number=56,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='CU', index=57, number=57,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='CW', index=58, number=58,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='CY', index=59, number=59,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='CZ', index=60, number=60,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='DK', index=61, number=61,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='DJ', index=62, number=62,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='DM', index=63, number=63,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='DO', index=64, number=64,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='EC', index=65, number=65,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='EG', index=66, number=66,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='SV', index=67, number=67,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='GQ', index=68, number=68,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ER', index=69, number=69,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='EE', index=70, number=70,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ET', index=71, number=71,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='FK', index=72, number=72,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='FO', index=73, number=73,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='FJ', index=74, number=74,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='FI', index=75, number=75,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='FR', index=76, number=76,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='GF', index=77, number=77,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='PF', index=78, number=78,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='TF', index=79, number=79,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='GA', index=80, number=80,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='GM', index=81, number=81,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='GE', index=82, number=82,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='DE', index=83, number=83,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='GH', index=84, number=84,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='GI', index=85, number=85,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='GR', index=86, number=86,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='GL', index=87, number=87,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='GD', index=88, number=88,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='GP', index=89, number=89,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='GU', index=90, number=90,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='GT', index=91, number=91,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='GG', index=92, number=92,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='GN', index=93, number=93,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='GW', index=94, number=94,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='GY', index=95, number=95,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='HT', index=96, number=96,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='HM', index=97, number=97,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='VA', index=98, number=98,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='HN', index=99, number=99,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='HK', index=100, number=100,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='HU', index=101, number=101,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='IS', index=102, number=102,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='IN', index=103, number=103,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ID', index=104, number=104,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='IR', index=105, number=105,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='IQ', index=106, number=106,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='IE', index=107, number=107,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='IM', index=108, number=108,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='IL', index=109, number=109,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='IT', index=110, number=110,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='JM', index=111, number=111,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='JP', index=112, number=112,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='JE', index=113, number=113,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='JO', index=114, number=114,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='KZ', index=115, number=115,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='KE', index=116, number=116,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='KI', index=117, number=117,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='KP', index=118, number=118,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='KR', index=119, number=119,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='KW', index=120, number=120,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='KG', index=121, number=121,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='LA', index=122, number=122,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='LV', index=123, number=123,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='LB', index=124, number=124,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='LS', index=125, number=125,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='LR', index=126, number=126,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='LY', index=127, number=127,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='LI', index=128, number=128,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='LT', index=129, number=129,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='LU', index=130, number=130,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MO', index=131, number=131,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MK', index=132, number=132,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MG', index=133, number=133,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MW', index=134, number=134,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MY', index=135, number=135,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MV', index=136, number=136,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ML', index=137, number=137,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MT', index=138, number=138,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MH', index=139, number=139,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MQ', index=140, number=140,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MR', index=141, number=141,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MU', index=142, number=142,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='YT', index=143, number=143,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MX', index=144, number=144,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='FM', index=145, number=145,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MD', index=146, number=146,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MC', index=147, number=147,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MN', index=148, number=148,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ME', index=149, number=149,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MS', index=150, number=150,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MA', index=151, number=151,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MZ', index=152, number=152,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MM', index=153, number=153,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='NA', index=154, number=154,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='NR', index=155, number=155,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='NP', index=156, number=156,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='NL', index=157, number=157,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='NC', index=158, number=158,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='NZ', index=159, number=159,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='NI', index=160, number=160,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='NE', index=161, number=161,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='NG', index=162, number=162,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='NU', index=163, number=163,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='NF', index=164, number=164,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MP', index=165, number=165,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='NO', index=166, number=166,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='OM', index=167, number=167,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='PK', index=168, number=168,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='PW', index=169, number=169,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='PS', index=170, number=170,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='PA', index=171, number=171,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='PG', index=172, number=172,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='PY', index=173, number=173,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='PE', index=174, number=174,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='PH', index=175, number=175,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='PN', index=176, number=176,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='PL', index=177, number=177,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='PT', index=178, number=178,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='PR', index=179, number=179,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='QA', index=180, number=180,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='RE', index=181, number=181,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='RO', index=182, number=182,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='RU', index=183, number=183,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='RW', index=184, number=184,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='BL', index=185, number=185,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='SH', index=186, number=186,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='KN', index=187, number=187,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='LC', index=188, number=188,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='MF', index=189, number=189,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='PM', index=190, number=190,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='VC', index=191, number=191,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='WS', index=192, number=192,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='SM', index=193, number=193,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ST', index=194, number=194,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='SA', index=195, number=195,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='SN', index=196, number=196,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='RS', index=197, number=197,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='SC', index=198, number=198,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='SL', index=199, number=199,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='SG', index=200, number=200,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='SX', index=201, number=201,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='SK', index=202, number=202,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='SI', index=203, number=203,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='SB', index=204, number=204,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='SO', index=205, number=205,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ZA', index=206, number=206,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='GS', index=207, number=207,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='SS', index=208, number=208,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ES', index=209, number=209,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='LK', index=210, number=210,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='SD', index=211, number=211,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='SR', index=212, number=212,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='SJ', index=213, number=213,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='SZ', index=214, number=214,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='SE', index=215, number=215,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='CH', index=216, number=216,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='SY', index=217, number=217,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='TW', index=218, number=218,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='TJ', index=219, number=219,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='TZ', index=220, number=220,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='TH', index=221, number=221,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='TL', index=222, number=222,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='TG', index=223, number=223,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='TK', index=224, number=224,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='TO', index=225, number=225,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='TT', index=226, number=226,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='TN', index=227, number=227,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='TR', index=228, number=228,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='TM', index=229, number=229,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='TC', index=230, number=230,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='TV', index=231, number=231,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='UG', index=232, number=232,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='UA', index=233, number=233,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='AE', index=234, number=234,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='GB', index=235, number=235,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='US', index=236, number=236,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='UM', index=237, number=237,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='UY', index=238, number=238,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='UZ', index=239, number=239,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='VU', index=240, number=240,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='VE', index=241, number=241,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='VN', index=242, number=242,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='VG', index=243, number=243,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='VI', index=244, number=244,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='WF', index=245, number=245,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='EH', index=246, number=246,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='YE', index=247, number=247,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ZM', index=248, number=248,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='ZW', index=249, number=249,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R001', index=250, number=250,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R002', index=251, number=251,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R015', index=252, number=252,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R012', index=253, number=253,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R818', index=254, number=254,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R434', index=255, number=255,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R504', index=256, number=256,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R729', index=257, number=257,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R788', index=258, number=258,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R732', index=259, number=259,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R202', index=260, number=260,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R014', index=261, number=261,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R086', index=262, number=262,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R108', index=263, number=263,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R174', index=264, number=264,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R262', index=265, number=265,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R232', index=266, number=266,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R231', index=267, number=267,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R260', index=268, number=268,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R404', index=269, number=269,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R450', index=270, number=270,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R454', index=271, number=271,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R480', index=272, number=272,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R175', index=273, number=273,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R508', index=274, number=274,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R638', index=275, number=275,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R646', index=276, number=276,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R690', index=277, number=277,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R706', index=278, number=278,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R728', index=279, number=279,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R800', index=280, number=280,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R834', index=281, number=281,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R894', index=282, number=282,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R716', index=283, number=283,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R017', index=284, number=284,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R024', index=285, number=285,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R120', index=286, number=286,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R140', index=287, number=287,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R148', index=288, number=288,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R178', index=289, number=289,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R180', index=290, number=290,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R226', index=291, number=291,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R266', index=292, number=292,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R678', index=293, number=293,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R018', index=294, number=294,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R072', index=295, number=295,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R748', index=296, number=296,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R426', index=297, number=297,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R516', index=298, number=298,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R710', index=299, number=299,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R011', index=300, number=300,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R204', index=301, number=301,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R854', index=302, number=302,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R132', index=303, number=303,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R384', index=304, number=304,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R270', index=305, number=305,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R288', index=306, number=306,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R324', index=307, number=307,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R624', index=308, number=308,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R430', index=309, number=309,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R466', index=310, number=310,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R478', index=311, number=311,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R562', index=312, number=312,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R566', index=313, number=313,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R654', index=314, number=314,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R686', index=315, number=315,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R694', index=316, number=316,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R768', index=317, number=317,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R019', index=318, number=318,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R419', index=319, number=319,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R029', index=320, number=320,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R660', index=321, number=321,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R028', index=322, number=322,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R533', index=323, number=323,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R044', index=324, number=324,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R052', index=325, number=325,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R535', index=326, number=326,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R092', index=327, number=327,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R136', index=328, number=328,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R192', index=329, number=329,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R531', index=330, number=330,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R212', index=331, number=331,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R214', index=332, number=332,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R308', index=333, number=333,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R312', index=334, number=334,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R332', index=335, number=335,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R388', index=336, number=336,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R474', index=337, number=337,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R500', index=338, number=338,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R630', index=339, number=339,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R652', index=340, number=340,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R659', index=341, number=341,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R662', index=342, number=342,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R663', index=343, number=343,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R670', index=344, number=344,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R534', index=345, number=345,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R780', index=346, number=346,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R796', index=347, number=347,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R850', index=348, number=348,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R013', index=349, number=349,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R084', index=350, number=350,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R188', index=351, number=351,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R222', index=352, number=352,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R320', index=353, number=353,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R340', index=354, number=354,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R484', index=355, number=355,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R558', index=356, number=356,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R591', index=357, number=357,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R005', index=358, number=358,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R032', index=359, number=359,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R068', index=360, number=360,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R074', index=361, number=361,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R076', index=362, number=362,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R152', index=363, number=363,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R170', index=364, number=364,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R218', index=365, number=365,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R238', index=366, number=366,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R254', index=367, number=367,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R328', index=368, number=368,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R600', index=369, number=369,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R604', index=370, number=370,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R239', index=371, number=371,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R740', index=372, number=372,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R858', index=373, number=373,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R862', index=374, number=374,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R021', index=375, number=375,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R060', index=376, number=376,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R124', index=377, number=377,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R304', index=378, number=378,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R666', index=379, number=379,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R840', index=380, number=380,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R010', index=381, number=381,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R142', index=382, number=382,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R143', index=383, number=383,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R398', index=384, number=384,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R417', index=385, number=385,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R762', index=386, number=386,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R795', index=387, number=387,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R860', index=388, number=388,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R030', index=389, number=389,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R156', index=390, number=390,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R344', index=391, number=391,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R446', index=392, number=392,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R408', index=393, number=393,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R392', index=394, number=394,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R496', index=395, number=395,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R410', index=396, number=396,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R035', index=397, number=397,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R096', index=398, number=398,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R116', index=399, number=399,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R360', index=400, number=400,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R418', index=401, number=401,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R458', index=402, number=402,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R104', index=403, number=403,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R608', index=404, number=404,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R702', index=405, number=405,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R764', index=406, number=406,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R626', index=407, number=407,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R704', index=408, number=408,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R034', index=409, number=409,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R004', index=410, number=410,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R050', index=411, number=411,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R064', index=412, number=412,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R356', index=413, number=413,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R364', index=414, number=414,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R462', index=415, number=415,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R524', index=416, number=416,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R586', index=417, number=417,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R144', index=418, number=418,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R145', index=419, number=419,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R051', index=420, number=420,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R031', index=421, number=421,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R048', index=422, number=422,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R196', index=423, number=423,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R268', index=424, number=424,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R368', index=425, number=425,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R376', index=426, number=426,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R400', index=427, number=427,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R414', index=428, number=428,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R422', index=429, number=429,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R512', index=430, number=430,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R634', index=431, number=431,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R682', index=432, number=432,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R275', index=433, number=433,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R760', index=434, number=434,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R792', index=435, number=435,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R784', index=436, number=436,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R887', index=437, number=437,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R150', index=438, number=438,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R151', index=439, number=439,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R112', index=440, number=440,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R100', index=441, number=441,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R203', index=442, number=442,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R348', index=443, number=443,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R616', index=444, number=444,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R498', index=445, number=445,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R642', index=446, number=446,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R643', index=447, number=447,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R703', index=448, number=448,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R804', index=449, number=449,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R154', index=450, number=450,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R248', index=451, number=451,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R830', index=452, number=452,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R831', index=453, number=453,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R832', index=454, number=454,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R680', index=455, number=455,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R208', index=456, number=456,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R233', index=457, number=457,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R234', index=458, number=458,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R246', index=459, number=459,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R352', index=460, number=460,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R372', index=461, number=461,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R833', index=462, number=462,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R428', index=463, number=463,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R440', index=464, number=464,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R578', index=465, number=465,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R744', index=466, number=466,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R752', index=467, number=467,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R826', index=468, number=468,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R039', index=469, number=469,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R008', index=470, number=470,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R020', index=471, number=471,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R070', index=472, number=472,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R191', index=473, number=473,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R292', index=474, number=474,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R300', index=475, number=475,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R336', index=476, number=476,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R380', index=477, number=477,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R470', index=478, number=478,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R499', index=479, number=479,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R807', index=480, number=480,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R620', index=481, number=481,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R674', index=482, number=482,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R688', index=483, number=483,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R705', index=484, number=484,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R724', index=485, number=485,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R155', index=486, number=486,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R040', index=487, number=487,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R056', index=488, number=488,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R250', index=489, number=489,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R276', index=490, number=490,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R438', index=491, number=491,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R442', index=492, number=492,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R492', index=493, number=493,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R528', index=494, number=494,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R756', index=495, number=495,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R009', index=496, number=496,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R053', index=497, number=497,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R036', index=498, number=498,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R162', index=499, number=499,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R166', index=500, number=500,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R334', index=501, number=501,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R554', index=502, number=502,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R574', index=503, number=503,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R054', index=504, number=504,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R242', index=505, number=505,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R540', index=506, number=506,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R598', index=507, number=507,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R090', index=508, number=508,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R548', index=509, number=509,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R057', index=510, number=510,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R316', index=511, number=511,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R296', index=512, number=512,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R584', index=513, number=513,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R583', index=514, number=514,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R520', index=515, number=515,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R580', index=516, number=516,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R585', index=517, number=517,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R581', index=518, number=518,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R061', index=519, number=519,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R016', index=520, number=520,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R184', index=521, number=521,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R258', index=522, number=522,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R570', index=523, number=523,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R612', index=524, number=524,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R882', index=525, number=525,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R772', index=526, number=526,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R776', index=527, number=527,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R798', index=528, number=528,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='R876', index=529, number=529,\n      options=None,\n      type=None),\n  ],\n  containing_type=None,\n  options=None,\n  serialized_start=5337,\n  serialized_end=10561,\n)\n_sym_db.RegisterEnumDescriptor(_LOCATION_COUNTRY)\n\n\n_CLAIM = _descriptor.Descriptor(\n  name='Claim',\n  full_name='pb.Claim',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='stream', full_name='pb.Claim.stream', index=0,\n      number=1, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='channel', full_name='pb.Claim.channel', index=1,\n      number=2, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='collection', full_name='pb.Claim.collection', index=2,\n      number=3, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='repost', full_name='pb.Claim.repost', index=3,\n      number=4, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='title', full_name='pb.Claim.title', index=4,\n      number=8, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='description', full_name='pb.Claim.description', index=5,\n      number=9, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='thumbnail', full_name='pb.Claim.thumbnail', index=6,\n      number=10, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='tags', full_name='pb.Claim.tags', index=7,\n      number=11, type=9, cpp_type=9, label=3,\n      has_default_value=False, default_value=[],\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='languages', full_name='pb.Claim.languages', index=8,\n      number=12, type=11, cpp_type=10, label=3,\n      has_default_value=False, default_value=[],\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='locations', full_name='pb.Claim.locations', index=9,\n      number=13, type=11, cpp_type=10, label=3,\n      has_default_value=False, default_value=[],\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n    _descriptor.OneofDescriptor(\n      name='type', full_name='pb.Claim.type',\n      index=0, containing_type=None, fields=[]),\n  ],\n  serialized_start=20,\n  serialized_end=319,\n)\n\n\n_STREAM = _descriptor.Descriptor(\n  name='Stream',\n  full_name='pb.Stream',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='source', full_name='pb.Stream.source', index=0,\n      number=1, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='author', full_name='pb.Stream.author', index=1,\n      number=2, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='license', full_name='pb.Stream.license', index=2,\n      number=3, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='license_url', full_name='pb.Stream.license_url', index=3,\n      number=4, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='release_time', full_name='pb.Stream.release_time', index=4,\n      number=5, type=3, cpp_type=2, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='fee', full_name='pb.Stream.fee', index=5,\n      number=6, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='image', full_name='pb.Stream.image', index=6,\n      number=10, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='video', full_name='pb.Stream.video', index=7,\n      number=11, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='audio', full_name='pb.Stream.audio', index=8,\n      number=12, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='software', full_name='pb.Stream.software', index=9,\n      number=13, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n    _descriptor.OneofDescriptor(\n      name='type', full_name='pb.Stream.type',\n      index=0, containing_type=None, fields=[]),\n  ],\n  serialized_start=322,\n  serialized_end=582,\n)\n\n\n_CHANNEL = _descriptor.Descriptor(\n  name='Channel',\n  full_name='pb.Channel',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='public_key', full_name='pb.Channel.public_key', index=0,\n      number=1, type=12, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\"),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='email', full_name='pb.Channel.email', index=1,\n      number=2, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='website_url', full_name='pb.Channel.website_url', index=2,\n      number=3, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='cover', full_name='pb.Channel.cover', index=3,\n      number=4, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='featured', full_name='pb.Channel.featured', index=4,\n      number=5, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=584,\n  serialized_end=709,\n)\n\n\n_CLAIMREFERENCE = _descriptor.Descriptor(\n  name='ClaimReference',\n  full_name='pb.ClaimReference',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='claim_hash', full_name='pb.ClaimReference.claim_hash', index=0,\n      number=1, type=12, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\"),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=711,\n  serialized_end=747,\n)\n\n\n_CLAIMLIST = _descriptor.Descriptor(\n  name='ClaimList',\n  full_name='pb.ClaimList',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='list_type', full_name='pb.ClaimList.list_type', index=0,\n      number=1, type=14, cpp_type=8, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='claim_references', full_name='pb.ClaimList.claim_references', index=1,\n      number=2, type=11, cpp_type=10, label=3,\n      has_default_value=False, default_value=[],\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n    _CLAIMLIST_LISTTYPE,\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=750,\n  serialized_end=894,\n)\n\n\n_SOURCE = _descriptor.Descriptor(\n  name='Source',\n  full_name='pb.Source',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='hash', full_name='pb.Source.hash', index=0,\n      number=1, type=12, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\"),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='name', full_name='pb.Source.name', index=1,\n      number=2, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='size', full_name='pb.Source.size', index=2,\n      number=3, type=4, cpp_type=4, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='media_type', full_name='pb.Source.media_type', index=3,\n      number=4, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='url', full_name='pb.Source.url', index=4,\n      number=5, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='sd_hash', full_name='pb.Source.sd_hash', index=5,\n      number=6, type=12, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\"),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='bt_infohash', full_name='pb.Source.bt_infohash', index=6,\n      number=7, type=12, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\"),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=896,\n  serialized_end=1017,\n)\n\n\n_FEE = _descriptor.Descriptor(\n  name='Fee',\n  full_name='pb.Fee',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='currency', full_name='pb.Fee.currency', index=0,\n      number=1, type=14, cpp_type=8, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='address', full_name='pb.Fee.address', index=1,\n      number=2, type=12, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\"),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='amount', full_name='pb.Fee.amount', index=2,\n      number=3, type=4, cpp_type=4, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n    _FEE_CURRENCY,\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=1020,\n  serialized_end=1155,\n)\n\n\n_IMAGE = _descriptor.Descriptor(\n  name='Image',\n  full_name='pb.Image',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='width', full_name='pb.Image.width', index=0,\n      number=1, type=13, cpp_type=3, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='height', full_name='pb.Image.height', index=1,\n      number=2, type=13, cpp_type=3, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=1157,\n  serialized_end=1195,\n)\n\n\n_VIDEO = _descriptor.Descriptor(\n  name='Video',\n  full_name='pb.Video',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='width', full_name='pb.Video.width', index=0,\n      number=1, type=13, cpp_type=3, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='height', full_name='pb.Video.height', index=1,\n      number=2, type=13, cpp_type=3, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='duration', full_name='pb.Video.duration', index=2,\n      number=3, type=13, cpp_type=3, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='audio', full_name='pb.Video.audio', index=3,\n      number=15, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=1197,\n  serialized_end=1279,\n)\n\n\n_AUDIO = _descriptor.Descriptor(\n  name='Audio',\n  full_name='pb.Audio',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='duration', full_name='pb.Audio.duration', index=0,\n      number=1, type=13, cpp_type=3, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=1281,\n  serialized_end=1306,\n)\n\n\n_SOFTWARE = _descriptor.Descriptor(\n  name='Software',\n  full_name='pb.Software',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='os', full_name='pb.Software.os', index=0,\n      number=1, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n    _SOFTWARE_OS,\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=1308,\n  serialized_end=1416,\n)\n\n\n_LANGUAGE = _descriptor.Descriptor(\n  name='Language',\n  full_name='pb.Language',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='language', full_name='pb.Language.language', index=0,\n      number=1, type=14, cpp_type=8, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='script', full_name='pb.Language.script', index=1,\n      number=2, type=14, cpp_type=8, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='region', full_name='pb.Language.region', index=2,\n      number=3, type=14, cpp_type=8, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n    _LANGUAGE_LANGUAGE,\n    _LANGUAGE_SCRIPT,\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=1419,\n  serialized_end=5202,\n)\n\n\n_LOCATION = _descriptor.Descriptor(\n  name='Location',\n  full_name='pb.Location',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='country', full_name='pb.Location.country', index=0,\n      number=1, type=14, cpp_type=8, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='state', full_name='pb.Location.state', index=1,\n      number=2, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='city', full_name='pb.Location.city', index=2,\n      number=3, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='code', full_name='pb.Location.code', index=3,\n      number=4, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='latitude', full_name='pb.Location.latitude', index=4,\n      number=5, type=17, cpp_type=1, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='longitude', full_name='pb.Location.longitude', index=5,\n      number=6, type=17, cpp_type=1, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n    _LOCATION_COUNTRY,\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=5205,\n  serialized_end=10561,\n)\n\n_CLAIM.fields_by_name['stream'].message_type = _STREAM\n_CLAIM.fields_by_name['channel'].message_type = _CHANNEL\n_CLAIM.fields_by_name['collection'].message_type = _CLAIMLIST\n_CLAIM.fields_by_name['repost'].message_type = _CLAIMREFERENCE\n_CLAIM.fields_by_name['thumbnail'].message_type = _SOURCE\n_CLAIM.fields_by_name['languages'].message_type = _LANGUAGE\n_CLAIM.fields_by_name['locations'].message_type = _LOCATION\n_CLAIM.oneofs_by_name['type'].fields.append(\n  _CLAIM.fields_by_name['stream'])\n_CLAIM.fields_by_name['stream'].containing_oneof = _CLAIM.oneofs_by_name['type']\n_CLAIM.oneofs_by_name['type'].fields.append(\n  _CLAIM.fields_by_name['channel'])\n_CLAIM.fields_by_name['channel'].containing_oneof = _CLAIM.oneofs_by_name['type']\n_CLAIM.oneofs_by_name['type'].fields.append(\n  _CLAIM.fields_by_name['collection'])\n_CLAIM.fields_by_name['collection'].containing_oneof = _CLAIM.oneofs_by_name['type']\n_CLAIM.oneofs_by_name['type'].fields.append(\n  _CLAIM.fields_by_name['repost'])\n_CLAIM.fields_by_name['repost'].containing_oneof = _CLAIM.oneofs_by_name['type']\n_STREAM.fields_by_name['source'].message_type = _SOURCE\n_STREAM.fields_by_name['fee'].message_type = _FEE\n_STREAM.fields_by_name['image'].message_type = _IMAGE\n_STREAM.fields_by_name['video'].message_type = _VIDEO\n_STREAM.fields_by_name['audio'].message_type = _AUDIO\n_STREAM.fields_by_name['software'].message_type = _SOFTWARE\n_STREAM.oneofs_by_name['type'].fields.append(\n  _STREAM.fields_by_name['image'])\n_STREAM.fields_by_name['image'].containing_oneof = _STREAM.oneofs_by_name['type']\n_STREAM.oneofs_by_name['type'].fields.append(\n  _STREAM.fields_by_name['video'])\n_STREAM.fields_by_name['video'].containing_oneof = _STREAM.oneofs_by_name['type']\n_STREAM.oneofs_by_name['type'].fields.append(\n  _STREAM.fields_by_name['audio'])\n_STREAM.fields_by_name['audio'].containing_oneof = _STREAM.oneofs_by_name['type']\n_STREAM.oneofs_by_name['type'].fields.append(\n  _STREAM.fields_by_name['software'])\n_STREAM.fields_by_name['software'].containing_oneof = _STREAM.oneofs_by_name['type']\n_CHANNEL.fields_by_name['cover'].message_type = _SOURCE\n_CHANNEL.fields_by_name['featured'].message_type = _CLAIMLIST\n_CLAIMLIST.fields_by_name['list_type'].enum_type = _CLAIMLIST_LISTTYPE\n_CLAIMLIST.fields_by_name['claim_references'].message_type = _CLAIMREFERENCE\n_CLAIMLIST_LISTTYPE.containing_type = _CLAIMLIST\n_FEE.fields_by_name['currency'].enum_type = _FEE_CURRENCY\n_FEE_CURRENCY.containing_type = _FEE\n_VIDEO.fields_by_name['audio'].message_type = _AUDIO\n_SOFTWARE_OS.containing_type = _SOFTWARE\n_LANGUAGE.fields_by_name['language'].enum_type = _LANGUAGE_LANGUAGE\n_LANGUAGE.fields_by_name['script'].enum_type = _LANGUAGE_SCRIPT\n_LANGUAGE.fields_by_name['region'].enum_type = _LOCATION_COUNTRY\n_LANGUAGE_LANGUAGE.containing_type = _LANGUAGE\n_LANGUAGE_SCRIPT.containing_type = _LANGUAGE\n_LOCATION.fields_by_name['country'].enum_type = _LOCATION_COUNTRY\n_LOCATION_COUNTRY.containing_type = _LOCATION\nDESCRIPTOR.message_types_by_name['Claim'] = _CLAIM\nDESCRIPTOR.message_types_by_name['Stream'] = _STREAM\nDESCRIPTOR.message_types_by_name['Channel'] = _CHANNEL\nDESCRIPTOR.message_types_by_name['ClaimReference'] = _CLAIMREFERENCE\nDESCRIPTOR.message_types_by_name['ClaimList'] = _CLAIMLIST\nDESCRIPTOR.message_types_by_name['Source'] = _SOURCE\nDESCRIPTOR.message_types_by_name['Fee'] = _FEE\nDESCRIPTOR.message_types_by_name['Image'] = _IMAGE\nDESCRIPTOR.message_types_by_name['Video'] = _VIDEO\nDESCRIPTOR.message_types_by_name['Audio'] = _AUDIO\nDESCRIPTOR.message_types_by_name['Software'] = _SOFTWARE\nDESCRIPTOR.message_types_by_name['Language'] = _LANGUAGE\nDESCRIPTOR.message_types_by_name['Location'] = _LOCATION\n\nClaim = _reflection.GeneratedProtocolMessageType('Claim', (_message.Message,), dict(\n  DESCRIPTOR = _CLAIM,\n  __module__ = 'claim_pb2'\n  # @@protoc_insertion_point(class_scope:pb.Claim)\n  ))\n_sym_db.RegisterMessage(Claim)\n\nStream = _reflection.GeneratedProtocolMessageType('Stream', (_message.Message,), dict(\n  DESCRIPTOR = _STREAM,\n  __module__ = 'claim_pb2'\n  # @@protoc_insertion_point(class_scope:pb.Stream)\n  ))\n_sym_db.RegisterMessage(Stream)\n\nChannel = _reflection.GeneratedProtocolMessageType('Channel', (_message.Message,), dict(\n  DESCRIPTOR = _CHANNEL,\n  __module__ = 'claim_pb2'\n  # @@protoc_insertion_point(class_scope:pb.Channel)\n  ))\n_sym_db.RegisterMessage(Channel)\n\nClaimReference = _reflection.GeneratedProtocolMessageType('ClaimReference', (_message.Message,), dict(\n  DESCRIPTOR = _CLAIMREFERENCE,\n  __module__ = 'claim_pb2'\n  # @@protoc_insertion_point(class_scope:pb.ClaimReference)\n  ))\n_sym_db.RegisterMessage(ClaimReference)\n\nClaimList = _reflection.GeneratedProtocolMessageType('ClaimList', (_message.Message,), dict(\n  DESCRIPTOR = _CLAIMLIST,\n  __module__ = 'claim_pb2'\n  # @@protoc_insertion_point(class_scope:pb.ClaimList)\n  ))\n_sym_db.RegisterMessage(ClaimList)\n\nSource = _reflection.GeneratedProtocolMessageType('Source', (_message.Message,), dict(\n  DESCRIPTOR = _SOURCE,\n  __module__ = 'claim_pb2'\n  # @@protoc_insertion_point(class_scope:pb.Source)\n  ))\n_sym_db.RegisterMessage(Source)\n\nFee = _reflection.GeneratedProtocolMessageType('Fee', (_message.Message,), dict(\n  DESCRIPTOR = _FEE,\n  __module__ = 'claim_pb2'\n  # @@protoc_insertion_point(class_scope:pb.Fee)\n  ))\n_sym_db.RegisterMessage(Fee)\n\nImage = _reflection.GeneratedProtocolMessageType('Image', (_message.Message,), dict(\n  DESCRIPTOR = _IMAGE,\n  __module__ = 'claim_pb2'\n  # @@protoc_insertion_point(class_scope:pb.Image)\n  ))\n_sym_db.RegisterMessage(Image)\n\nVideo = _reflection.GeneratedProtocolMessageType('Video', (_message.Message,), dict(\n  DESCRIPTOR = _VIDEO,\n  __module__ = 'claim_pb2'\n  # @@protoc_insertion_point(class_scope:pb.Video)\n  ))\n_sym_db.RegisterMessage(Video)\n\nAudio = _reflection.GeneratedProtocolMessageType('Audio', (_message.Message,), dict(\n  DESCRIPTOR = _AUDIO,\n  __module__ = 'claim_pb2'\n  # @@protoc_insertion_point(class_scope:pb.Audio)\n  ))\n_sym_db.RegisterMessage(Audio)\n\nSoftware = _reflection.GeneratedProtocolMessageType('Software', (_message.Message,), dict(\n  DESCRIPTOR = _SOFTWARE,\n  __module__ = 'claim_pb2'\n  # @@protoc_insertion_point(class_scope:pb.Software)\n  ))\n_sym_db.RegisterMessage(Software)\n\nLanguage = _reflection.GeneratedProtocolMessageType('Language', (_message.Message,), dict(\n  DESCRIPTOR = _LANGUAGE,\n  __module__ = 'claim_pb2'\n  # @@protoc_insertion_point(class_scope:pb.Language)\n  ))\n_sym_db.RegisterMessage(Language)\n\nLocation = _reflection.GeneratedProtocolMessageType('Location', (_message.Message,), dict(\n  DESCRIPTOR = _LOCATION,\n  __module__ = 'claim_pb2'\n  # @@protoc_insertion_point(class_scope:pb.Location)\n  ))\n_sym_db.RegisterMessage(Location)\n\n\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "lbry/schema/types/v2/purchase_pb2.py",
    "content": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: purchase.proto\n\nimport sys\n_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import message as _message\nfrom google.protobuf import reflection as _reflection\nfrom google.protobuf import symbol_database as _symbol_database\nfrom google.protobuf import descriptor_pb2\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor.FileDescriptor(\n  name='purchase.proto',\n  package='pb',\n  syntax='proto3',\n  serialized_pb=_b('\\n\\x0epurchase.proto\\x12\\x02pb\\\"\\x1e\\n\\x08Purchase\\x12\\x12\\n\\nclaim_hash\\x18\\x01 \\x01(\\x0c\\x62\\x06proto3')\n)\n_sym_db.RegisterFileDescriptor(DESCRIPTOR)\n\n\n\n\n_PURCHASE = _descriptor.Descriptor(\n  name='Purchase',\n  full_name='pb.Purchase',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='claim_hash', full_name='pb.Purchase.claim_hash', index=0,\n      number=1, type=12, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\"),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=22,\n  serialized_end=52,\n)\n\nDESCRIPTOR.message_types_by_name['Purchase'] = _PURCHASE\n\nPurchase = _reflection.GeneratedProtocolMessageType('Purchase', (_message.Message,), dict(\n  DESCRIPTOR = _PURCHASE,\n  __module__ = 'purchase_pb2'\n  # @@protoc_insertion_point(class_scope:pb.Purchase)\n  ))\n_sym_db.RegisterMessage(Purchase)\n\n\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "lbry/schema/types/v2/result_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: result.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import message as _message\nfrom google.protobuf import reflection as _reflection\nfrom google.protobuf import symbol_database as _symbol_database\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor.FileDescriptor(\n  name='result.proto',\n  package='pb',\n  syntax='proto3',\n  serialized_options=b'Z$github.com/lbryio/hub/protobuf/go/pb',\n  create_key=_descriptor._internal_create_key,\n  serialized_pb=b'\\n\\x0cresult.proto\\x12\\x02pb\\\"\\x97\\x01\\n\\x07Outputs\\x12\\x18\\n\\x04txos\\x18\\x01 \\x03(\\x0b\\x32\\n.pb.Output\\x12\\x1e\\n\\nextra_txos\\x18\\x02 \\x03(\\x0b\\x32\\n.pb.Output\\x12\\r\\n\\x05total\\x18\\x03 \\x01(\\r\\x12\\x0e\\n\\x06offset\\x18\\x04 \\x01(\\r\\x12\\x1c\\n\\x07\\x62locked\\x18\\x05 \\x03(\\x0b\\x32\\x0b.pb.Blocked\\x12\\x15\\n\\rblocked_total\\x18\\x06 \\x01(\\r\\\"{\\n\\x06Output\\x12\\x0f\\n\\x07tx_hash\\x18\\x01 \\x01(\\x0c\\x12\\x0c\\n\\x04nout\\x18\\x02 \\x01(\\r\\x12\\x0e\\n\\x06height\\x18\\x03 \\x01(\\r\\x12\\x1e\\n\\x05\\x63laim\\x18\\x07 \\x01(\\x0b\\x32\\r.pb.ClaimMetaH\\x00\\x12\\x1a\\n\\x05\\x65rror\\x18\\x0f \\x01(\\x0b\\x32\\t.pb.ErrorH\\x00\\x42\\x06\\n\\x04meta\\\"\\xe6\\x02\\n\\tClaimMeta\\x12\\x1b\\n\\x07\\x63hannel\\x18\\x01 \\x01(\\x0b\\x32\\n.pb.Output\\x12\\x1a\\n\\x06repost\\x18\\x02 \\x01(\\x0b\\x32\\n.pb.Output\\x12\\x11\\n\\tshort_url\\x18\\x03 \\x01(\\t\\x12\\x15\\n\\rcanonical_url\\x18\\x04 \\x01(\\t\\x12\\x16\\n\\x0eis_controlling\\x18\\x05 \\x01(\\x08\\x12\\x18\\n\\x10take_over_height\\x18\\x06 \\x01(\\r\\x12\\x17\\n\\x0f\\x63reation_height\\x18\\x07 \\x01(\\r\\x12\\x19\\n\\x11\\x61\\x63tivation_height\\x18\\x08 \\x01(\\r\\x12\\x19\\n\\x11\\x65xpiration_height\\x18\\t \\x01(\\r\\x12\\x19\\n\\x11\\x63laims_in_channel\\x18\\n \\x01(\\r\\x12\\x10\\n\\x08reposted\\x18\\x0b \\x01(\\r\\x12\\x18\\n\\x10\\x65\\x66\\x66\\x65\\x63tive_amount\\x18\\x14 \\x01(\\x04\\x12\\x16\\n\\x0esupport_amount\\x18\\x15 \\x01(\\x04\\x12\\x16\\n\\x0etrending_score\\x18\\x16 \\x01(\\x01\\\"\\x94\\x01\\n\\x05\\x45rror\\x12\\x1c\\n\\x04\\x63ode\\x18\\x01 \\x01(\\x0e\\x32\\x0e.pb.Error.Code\\x12\\x0c\\n\\x04text\\x18\\x02 \\x01(\\t\\x12\\x1c\\n\\x07\\x62locked\\x18\\x03 \\x01(\\x0b\\x32\\x0b.pb.Blocked\\\"A\\n\\x04\\x43ode\\x12\\x10\\n\\x0cUNKNOWN_CODE\\x10\\x00\\x12\\r\\n\\tNOT_FOUND\\x10\\x01\\x12\\x0b\\n\\x07INVALID\\x10\\x02\\x12\\x0b\\n\\x07\\x42LOCKED\\x10\\x03\\\"5\\n\\x07\\x42locked\\x12\\r\\n\\x05\\x63ount\\x18\\x01 \\x01(\\r\\x12\\x1b\\n\\x07\\x63hannel\\x18\\x02 \\x01(\\x0b\\x32\\n.pb.OutputB&Z$github.com/lbryio/hub/protobuf/go/pbb\\x06proto3'\n)\n\n\n\n_ERROR_CODE = _descriptor.EnumDescriptor(\n  name='Code',\n  full_name='pb.Error.Code',\n  filename=None,\n  file=DESCRIPTOR,\n  create_key=_descriptor._internal_create_key,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='UNKNOWN_CODE', index=0, number=0,\n      serialized_options=None,\n      type=None,\n      create_key=_descriptor._internal_create_key),\n    _descriptor.EnumValueDescriptor(\n      name='NOT_FOUND', index=1, number=1,\n      serialized_options=None,\n      type=None,\n      create_key=_descriptor._internal_create_key),\n    _descriptor.EnumValueDescriptor(\n      name='INVALID', index=2, number=2,\n      serialized_options=None,\n      type=None,\n      create_key=_descriptor._internal_create_key),\n    _descriptor.EnumValueDescriptor(\n      name='BLOCKED', index=3, number=3,\n      serialized_options=None,\n      type=None,\n      create_key=_descriptor._internal_create_key),\n  ],\n  containing_type=None,\n  serialized_options=None,\n  serialized_start=744,\n  serialized_end=809,\n)\n_sym_db.RegisterEnumDescriptor(_ERROR_CODE)\n\n\n_OUTPUTS = _descriptor.Descriptor(\n  name='Outputs',\n  full_name='pb.Outputs',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  create_key=_descriptor._internal_create_key,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='txos', full_name='pb.Outputs.txos', index=0,\n      number=1, type=11, cpp_type=10, label=3,\n      has_default_value=False, default_value=[],\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='extra_txos', full_name='pb.Outputs.extra_txos', index=1,\n      number=2, type=11, cpp_type=10, label=3,\n      has_default_value=False, default_value=[],\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='total', full_name='pb.Outputs.total', index=2,\n      number=3, type=13, cpp_type=3, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='offset', full_name='pb.Outputs.offset', index=3,\n      number=4, type=13, cpp_type=3, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='blocked', full_name='pb.Outputs.blocked', index=4,\n      number=5, type=11, cpp_type=10, label=3,\n      has_default_value=False, default_value=[],\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='blocked_total', full_name='pb.Outputs.blocked_total', index=5,\n      number=6, type=13, cpp_type=3, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  serialized_options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=21,\n  serialized_end=172,\n)\n\n\n_OUTPUT = _descriptor.Descriptor(\n  name='Output',\n  full_name='pb.Output',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  create_key=_descriptor._internal_create_key,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='tx_hash', full_name='pb.Output.tx_hash', index=0,\n      number=1, type=12, cpp_type=9, label=1,\n      has_default_value=False, default_value=b\"\",\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='nout', full_name='pb.Output.nout', index=1,\n      number=2, type=13, cpp_type=3, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='height', full_name='pb.Output.height', index=2,\n      number=3, type=13, cpp_type=3, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='claim', full_name='pb.Output.claim', index=3,\n      number=7, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='error', full_name='pb.Output.error', index=4,\n      number=15, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  serialized_options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n    _descriptor.OneofDescriptor(\n      name='meta', full_name='pb.Output.meta',\n      index=0, containing_type=None,\n      create_key=_descriptor._internal_create_key,\n    fields=[]),\n  ],\n  serialized_start=174,\n  serialized_end=297,\n)\n\n\n_CLAIMMETA = _descriptor.Descriptor(\n  name='ClaimMeta',\n  full_name='pb.ClaimMeta',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  create_key=_descriptor._internal_create_key,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='channel', full_name='pb.ClaimMeta.channel', index=0,\n      number=1, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='repost', full_name='pb.ClaimMeta.repost', index=1,\n      number=2, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='short_url', full_name='pb.ClaimMeta.short_url', index=2,\n      number=3, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=b\"\".decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='canonical_url', full_name='pb.ClaimMeta.canonical_url', index=3,\n      number=4, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=b\"\".decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='is_controlling', full_name='pb.ClaimMeta.is_controlling', index=4,\n      number=5, type=8, cpp_type=7, label=1,\n      has_default_value=False, default_value=False,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='take_over_height', full_name='pb.ClaimMeta.take_over_height', index=5,\n      number=6, type=13, cpp_type=3, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='creation_height', full_name='pb.ClaimMeta.creation_height', index=6,\n      number=7, type=13, cpp_type=3, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='activation_height', full_name='pb.ClaimMeta.activation_height', index=7,\n      number=8, type=13, cpp_type=3, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='expiration_height', full_name='pb.ClaimMeta.expiration_height', index=8,\n      number=9, type=13, cpp_type=3, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='claims_in_channel', full_name='pb.ClaimMeta.claims_in_channel', index=9,\n      number=10, type=13, cpp_type=3, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='reposted', full_name='pb.ClaimMeta.reposted', index=10,\n      number=11, type=13, cpp_type=3, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='effective_amount', full_name='pb.ClaimMeta.effective_amount', index=11,\n      number=20, type=4, cpp_type=4, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='support_amount', full_name='pb.ClaimMeta.support_amount', index=12,\n      number=21, type=4, cpp_type=4, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='trending_score', full_name='pb.ClaimMeta.trending_score', index=13,\n      number=22, type=1, cpp_type=5, label=1,\n      has_default_value=False, default_value=float(0),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  serialized_options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=300,\n  serialized_end=658,\n)\n\n\n_ERROR = _descriptor.Descriptor(\n  name='Error',\n  full_name='pb.Error',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  create_key=_descriptor._internal_create_key,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='code', full_name='pb.Error.code', index=0,\n      number=1, type=14, cpp_type=8, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='text', full_name='pb.Error.text', index=1,\n      number=2, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=b\"\".decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='blocked', full_name='pb.Error.blocked', index=2,\n      number=3, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n    _ERROR_CODE,\n  ],\n  serialized_options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=661,\n  serialized_end=809,\n)\n\n\n_BLOCKED = _descriptor.Descriptor(\n  name='Blocked',\n  full_name='pb.Blocked',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  create_key=_descriptor._internal_create_key,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='count', full_name='pb.Blocked.count', index=0,\n      number=1, type=13, cpp_type=3, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n    _descriptor.FieldDescriptor(\n      name='channel', full_name='pb.Blocked.channel', index=1,\n      number=2, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  serialized_options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=811,\n  serialized_end=864,\n)\n\n_OUTPUTS.fields_by_name['txos'].message_type = _OUTPUT\n_OUTPUTS.fields_by_name['extra_txos'].message_type = _OUTPUT\n_OUTPUTS.fields_by_name['blocked'].message_type = _BLOCKED\n_OUTPUT.fields_by_name['claim'].message_type = _CLAIMMETA\n_OUTPUT.fields_by_name['error'].message_type = _ERROR\n_OUTPUT.oneofs_by_name['meta'].fields.append(\n  _OUTPUT.fields_by_name['claim'])\n_OUTPUT.fields_by_name['claim'].containing_oneof = _OUTPUT.oneofs_by_name['meta']\n_OUTPUT.oneofs_by_name['meta'].fields.append(\n  _OUTPUT.fields_by_name['error'])\n_OUTPUT.fields_by_name['error'].containing_oneof = _OUTPUT.oneofs_by_name['meta']\n_CLAIMMETA.fields_by_name['channel'].message_type = _OUTPUT\n_CLAIMMETA.fields_by_name['repost'].message_type = _OUTPUT\n_ERROR.fields_by_name['code'].enum_type = _ERROR_CODE\n_ERROR.fields_by_name['blocked'].message_type = _BLOCKED\n_ERROR_CODE.containing_type = _ERROR\n_BLOCKED.fields_by_name['channel'].message_type = _OUTPUT\nDESCRIPTOR.message_types_by_name['Outputs'] = _OUTPUTS\nDESCRIPTOR.message_types_by_name['Output'] = _OUTPUT\nDESCRIPTOR.message_types_by_name['ClaimMeta'] = _CLAIMMETA\nDESCRIPTOR.message_types_by_name['Error'] = _ERROR\nDESCRIPTOR.message_types_by_name['Blocked'] = _BLOCKED\n_sym_db.RegisterFileDescriptor(DESCRIPTOR)\n\nOutputs = _reflection.GeneratedProtocolMessageType('Outputs', (_message.Message,), {\n  'DESCRIPTOR' : _OUTPUTS,\n  '__module__' : 'result_pb2'\n  # @@protoc_insertion_point(class_scope:pb.Outputs)\n  })\n_sym_db.RegisterMessage(Outputs)\n\nOutput = _reflection.GeneratedProtocolMessageType('Output', (_message.Message,), {\n  'DESCRIPTOR' : _OUTPUT,\n  '__module__' : 'result_pb2'\n  # @@protoc_insertion_point(class_scope:pb.Output)\n  })\n_sym_db.RegisterMessage(Output)\n\nClaimMeta = _reflection.GeneratedProtocolMessageType('ClaimMeta', (_message.Message,), {\n  'DESCRIPTOR' : _CLAIMMETA,\n  '__module__' : 'result_pb2'\n  # @@protoc_insertion_point(class_scope:pb.ClaimMeta)\n  })\n_sym_db.RegisterMessage(ClaimMeta)\n\nError = _reflection.GeneratedProtocolMessageType('Error', (_message.Message,), {\n  'DESCRIPTOR' : _ERROR,\n  '__module__' : 'result_pb2'\n  # @@protoc_insertion_point(class_scope:pb.Error)\n  })\n_sym_db.RegisterMessage(Error)\n\nBlocked = _reflection.GeneratedProtocolMessageType('Blocked', (_message.Message,), {\n  'DESCRIPTOR' : _BLOCKED,\n  '__module__' : 'result_pb2'\n  # @@protoc_insertion_point(class_scope:pb.Blocked)\n  })\n_sym_db.RegisterMessage(Blocked)\n\n\nDESCRIPTOR._options = None\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "lbry/schema/types/v2/support_pb2.py",
    "content": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: support.proto\n\nimport sys\n_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import message as _message\nfrom google.protobuf import reflection as _reflection\nfrom google.protobuf import symbol_database as _symbol_database\nfrom google.protobuf import descriptor_pb2\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor.FileDescriptor(\n  name='support.proto',\n  package='pb',\n  syntax='proto3',\n  serialized_pb=_b('\\n\\rsupport.proto\\x12\\x02pb\\\")\\n\\x07Support\\x12\\r\\n\\x05\\x65moji\\x18\\x01 \\x01(\\t\\x12\\x0f\\n\\x07\\x63omment\\x18\\x02 \\x01(\\tb\\x06proto3')\n)\n_sym_db.RegisterFileDescriptor(DESCRIPTOR)\n\n\n\n\n_SUPPORT = _descriptor.Descriptor(\n  name='Support',\n  full_name='pb.Support',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='emoji', full_name='pb.Support.emoji', index=0,\n      number=1, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n    _descriptor.FieldDescriptor(\n      name='comment', full_name='pb.Support.comment', index=1,\n      number=2, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=21,\n  serialized_end=62,\n)\n\nDESCRIPTOR.message_types_by_name['Support'] = _SUPPORT\n\nSupport = _reflection.GeneratedProtocolMessageType('Support', (_message.Message,), dict(\n  DESCRIPTOR = _SUPPORT,\n  __module__ = 'support_pb2'\n  # @@protoc_insertion_point(class_scope:pb.Support)\n  ))\n_sym_db.RegisterMessage(Support)\n\n\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "lbry/schema/types/v2/wallet.json",
    "content": "{\n  \"title\": \"Wallet\",\n  \"description\": \"An LBC wallet\",\n  \"type\": \"object\",\n  \"required\": [\"name\", \"version\", \"accounts\", \"preferences\"],\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"name\": {\n      \"description\": \"Human readable name for this wallet\",\n      \"type\": \"string\"\n    },\n    \"version\": {\n      \"description\": \"Wallet spec version\",\n      \"type\": \"integer\",\n      \"$comment\": \"Should this be a string? We may need some sort of decimal type if we want exact decimal versions.\"\n    },\n    \"accounts\": {\n      \"description\": \"Accounts associated with this wallet\",\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"required\": [\"address_generator\", \"certificates\", \"encrypted\", \"ledger\", \"modified_on\", \"name\", \"private_key\", \"public_key\", \"seed\"],\n        \"additionalProperties\": false,\n        \"properties\": {\n          \"address_generator\": {\n            \"description\": \"Higher level manager of either singular or deterministically generated addresses\",\n            \"type\": \"object\",\n            \"oneOf\": [\n              {\n                \"required\": [\"name\", \"change\", \"receiving\"],\n                \"additionalProperties\": false,\n                \"properties\": {\n                  \"name\": {\n                    \"description\": \"type of address generator: a deterministic chain of addresses\",\n                    \"enum\": [\"deterministic-chain\"],\n                    \"type\": \"string\"\n                  },\n                  \"change\": {\n                    \"$ref\": \"#/$defs/address_manager\",\n                    \"description\": \"Manager for deterministically generated change address (not used for single address)\"\n                  },\n                  \"receiving\": {\n                    \"$ref\": \"#/$defs/address_manager\",\n                    \"description\": \"Manager for deterministically generated receiving address (not used for single address)\"\n                  }\n                }\n              }, {\n                \"required\": [\"name\"],\n                \"additionalProperties\": false,\n                \"properties\": {\n                  \"name\": {\n                    \"description\": \"type of address generator: a single address\",\n                    \"enum\": [\"single-address\"],\n                    \"type\": \"string\"\n                  }\n                }\n              }\n            ]\n          },\n          \"certificates\": {\n            \"type\": \"object\",\n            \"description\": \"Channel keys. Mapping from public key address to pem-formatted private key.\",\n            \"additionalProperties\": {\"type\": \"string\"}\n          },\n          \"encrypted\": {\n            \"type\": \"boolean\",\n            \"description\": \"Whether private key and seed are encrypted with a password\"\n          },\n          \"ledger\": {\n            \"description\": \"Which network to use\",\n            \"type\": \"string\",\n            \"examples\": [\n              \"lbc_mainnet\",\n              \"lbc_testnet\"\n            ]\n          },\n          \"modified_on\": {\n            \"description\": \"last modified time in Unix Time\",\n            \"type\": \"integer\"\n          },\n          \"name\": {\n            \"description\": \"Name for account, possibly human readable\",\n            \"type\": \"string\"\n          },\n          \"private_key\": {\n            \"description\": \"Private key for address if `address_generator` is a single address. Root of chain of private keys for addresses if `address_generator` is a deterministic chain of addresses. Encrypted if `encrypted` is true.\",\n            \"type\": \"string\"\n          },\n          \"public_key\": {\n            \"description\": \"Public key for address if `address_generator` is a single address. Root of chain of public keys for addresses if `address_generator` is a deterministic chain of addresses.\",\n            \"type\": \"string\"\n          },\n          \"seed\": {\n            \"description\": \"Human readable representation of `private_key`. encrypted if `encrypted` is set to `true`\",\n            \"type\": \"string\"\n          }\n        }\n      }\n    },\n    \"preferences\": {\n      \"description\": \"Timestamped application-level preferences. Values can be objects or of a primitive type.\",\n      \"$comment\": \"enable-sync is seen in example wallet. encrypt-on-disk is seen in example wallet. they both have a boolean `value` field. Do we want them explicitly defined here? local and shared seem to have at least a similar structure (type, value [yes, again], version), value being the free-form part. Should we define those here? Or can there be any key under preferences, and `value` be literally be anything in any form?\",\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"type\": \"object\",\n        \"required\": [\"ts\", \"value\"],\n        \"additionalProperties\": false,\n        \"properties\": {\n          \"ts\": {\n            \"type\": \"number\",\n            \"description\": \"When the item was set, in Unix time format.\",\n            \"$comment\": \"Do we want a string (decimal)?\"\n          },\n          \"value\": {\n            \"$comment\": \"Sometimes this has been an object, sometimes just a boolean. I don't want to prescribe anything.\"\n          }\n        }\n      }\n    }\n  },\n  \"$defs\": {\n    \"address_manager\": {\n      \"description\": \"Manager for deterministically generated addresses\",\n      \"type\": \"object\",\n      \"required\": [\"gap\", \"maximum_uses_per_address\"],\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"gap\": {\n          \"description\": \"Maximum allowed consecutive generated addresses with no transactions\",\n          \"type\": \"integer\"\n        },\n        \"maximum_uses_per_address\": {\n          \"description\": \"Maximum number of uses for each generated address\",\n          \"type\": \"integer\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lbry/schema/url.py",
    "content": "import re\nimport unicodedata\nfrom typing import NamedTuple, Tuple\n\n\ndef _create_url_regex():\n    # see https://spec.lbry.com/ and test_url.py\n    invalid_names_regex = \\\n        r\"[^=&#:$@%?;\\\"/\\\\<>%{}|^~`\\[\\]\" \\\n        r\"\\u0000-\\u0020\\uD800-\\uDFFF\\uFFFE-\\uFFFF]+\"\n\n    def _named(name, regex):\n        return \"(?P<\" + name + \">\" + regex + \")\"\n\n    def _group(regex):\n        return \"(?:\" + regex + \")\"\n\n    def _oneof(*choices):\n        return _group('|'.join(choices))\n\n    def _claim(name, prefix=\"\"):\n        return _group(\n            _named(name+\"_name\", prefix + invalid_names_regex) +\n            _oneof(\n                _group('[:#]' + _named(name+\"_claim_id\", \"[0-9a-f]{1,40}\")),\n                _group(r'\\$' + _named(name+\"_amount_order\", '[1-9][0-9]*'))\n            ) + '?'\n        )\n\n    return (\n        '^' +\n        _named(\"scheme\", \"lbry://\") + '?' +\n        _oneof(\n            _group(_claim(\"channel_with_stream\", \"@\") + \"/\" + _claim(\"stream_in_channel\")),\n            _claim(\"channel\", \"@\"),\n            _claim(\"stream\")\n        ) +\n        '$'\n    )\n\n\nURL_REGEX = _create_url_regex()\n\n\ndef normalize_name(name):\n    return unicodedata.normalize('NFD', name).casefold()\n\n\nclass PathSegment(NamedTuple):\n    name: str\n    claim_id: str = None\n    amount_order: int = None\n\n    @property\n    def normalized(self):\n        return normalize_name(self.name)\n\n    @property\n    def is_shortid(self):\n        return self.claim_id is not None and len(self.claim_id) < 40\n\n    @property\n    def is_fullid(self):\n        return self.claim_id is not None and len(self.claim_id) == 40\n\n    def to_dict(self):\n        q = {'name': self.name}\n        if self.claim_id is not None:\n            q['claim_id'] = self.claim_id\n        if self.amount_order is not None:\n            q['amount_order'] = self.amount_order\n        return q\n\n    def __str__(self):\n        if self.claim_id is not None:\n            return f\"{self.name}:{self.claim_id}\"\n        elif self.amount_order is not None:\n            return f\"{self.name}${self.amount_order}\"\n        return self.name\n\n\nclass URL(NamedTuple):\n    stream: PathSegment\n    channel: PathSegment\n\n    @property\n    def has_channel(self):\n        return self.channel is not None\n\n    @property\n    def has_stream(self):\n        return self.stream is not None\n\n    @property\n    def has_stream_in_channel(self):\n        return self.has_channel and self.has_stream\n\n    @property\n    def parts(self) -> Tuple:\n        if self.has_stream_in_channel:\n            return self.channel, self.stream\n        if self.has_channel:\n            return self.channel,\n        return self.stream,\n\n    def __str__(self):\n        return f\"lbry://{'/'.join(str(p) for p in self.parts)}\"\n\n    @classmethod\n    def parse(cls, url):\n        match = re.match(URL_REGEX, url)\n\n        if match is None:\n            raise ValueError('Invalid LBRY URL')\n\n        segments = {}\n        parts = match.groupdict()\n        for segment in ('channel', 'stream', 'channel_with_stream', 'stream_in_channel'):\n            if parts[f'{segment}_name'] is not None:\n                segments[segment] = PathSegment(\n                    parts[f'{segment}_name'],\n                    parts[f'{segment}_claim_id'],\n                    parts[f'{segment}_amount_order']\n                )\n\n        if 'channel_with_stream' in segments:\n            segments['channel'] = segments['channel_with_stream']\n            segments['stream'] = segments['stream_in_channel']\n\n        return cls(segments.get('stream', None), segments.get('channel', None))\n"
  },
  {
    "path": "lbry/stream/__init__.py",
    "content": ""
  },
  {
    "path": "lbry/stream/background_downloader.py",
    "content": "import asyncio\nimport logging\n\nfrom lbry.stream.downloader import StreamDownloader\n\n\nlog = logging.getLogger(__name__)\n\n\nclass BackgroundDownloader:\n    def __init__(self, conf, storage, blob_manager, dht_node=None):\n        self.storage = storage\n        self.blob_manager = blob_manager\n        self.node = dht_node\n        self.conf = conf\n\n    async def download_blobs(self, sd_hash):\n        downloader = StreamDownloader(asyncio.get_running_loop(), self.conf, self.blob_manager, sd_hash)\n        try:\n            await downloader.start(self.node, save_stream=False)\n            for blob_info in downloader.descriptor.blobs[:-1]:\n                await downloader.download_stream_blob(blob_info)\n        except ValueError:\n            return\n        except asyncio.CancelledError:\n            log.debug(\"Cancelled background downloader\")\n            raise\n        except Exception:\n            log.error(\"Unexpected download error on background downloader\")\n        finally:\n            downloader.stop()\n"
  },
  {
    "path": "lbry/stream/descriptor.py",
    "content": "import os\nimport json\nimport binascii\nimport logging\nimport typing\nimport asyncio\nimport time\nimport re\nfrom collections import OrderedDict\nfrom cryptography.hazmat.primitives.ciphers.algorithms import AES\nfrom lbry.blob import MAX_BLOB_SIZE\nfrom lbry.blob.blob_info import BlobInfo\nfrom lbry.blob.blob_file import AbstractBlob, BlobFile\nfrom lbry.utils import get_lbry_hash_obj\nfrom lbry.error import InvalidStreamDescriptorError\n\nlog = logging.getLogger(__name__)\n\nRE_ILLEGAL_FILENAME_CHARS = re.compile(\n    r'('\n    r'[<>:\"/\\\\|?*]+|'                  # Illegal characters\n    r'[\\x00-\\x1F]+|'                   # All characters in range 0-31\n    r'[ \\t]*(\\.)+[ \\t]*$|'             # Dots at the end\n    r'(^[ \\t]+|[ \\t]+$)|'              # Leading and trailing whitespace\n    r'^CON$|^PRN$|^AUX$|'              # Illegal names\n    r'^NUL$|^COM[1-9]$|^LPT[1-9]$'     # ...\n    r')'\n)\n\n\ndef format_sd_info(stream_name: str, key: str, suggested_file_name: str, stream_hash: str,\n                   blobs: typing.List[typing.Dict]) -> typing.Dict:\n    return {\n        \"stream_type\": \"lbryfile\",\n        \"stream_name\": stream_name,\n        \"key\": key,\n        \"suggested_file_name\": suggested_file_name,\n        \"stream_hash\": stream_hash,\n        \"blobs\": blobs\n    }\n\n\ndef random_iv_generator() -> typing.Generator[bytes, None, None]:\n    while 1:\n        yield os.urandom(AES.block_size // 8)\n\n\ndef read_bytes(file_path: str, offset: int, to_read: int):\n    with open(file_path, 'rb') as f:\n        f.seek(offset)\n        return f.read(to_read)\n\n\nasync def file_reader(file_path: str):\n    length = int(os.stat(file_path).st_size)\n    offset = 0\n\n    while offset < length:\n        bytes_to_read = min((length - offset), MAX_BLOB_SIZE - 1)\n        if not bytes_to_read:\n            break\n        blob_bytes = await asyncio.get_event_loop().run_in_executor(\n            None, read_bytes, file_path, offset, bytes_to_read\n        )\n        yield blob_bytes\n        offset += bytes_to_read\n\n\ndef sanitize_file_name(dirty_name: str, default_file_name: str = 'lbry_download'):\n    file_name, ext = os.path.splitext(dirty_name)\n    file_name = re.sub(RE_ILLEGAL_FILENAME_CHARS, '', file_name)\n    ext = re.sub(RE_ILLEGAL_FILENAME_CHARS, '', ext)\n\n    if not file_name:\n        log.warning('Unable to sanitize file name for %s, returning default value %s', dirty_name, default_file_name)\n        file_name = default_file_name\n    if len(ext) > 1:\n        file_name += ext\n\n    return file_name\n\n\nclass StreamDescriptor:\n    __slots__ = [\n        'loop',\n        'blob_dir',\n        'stream_name',\n        'key',\n        'suggested_file_name',\n        'blobs',\n        'stream_hash',\n        'sd_hash'\n    ]\n\n    def __init__(self, loop: asyncio.AbstractEventLoop, blob_dir: str, stream_name: str, key: str,\n                 suggested_file_name: str, blobs: typing.List[BlobInfo], stream_hash: typing.Optional[str] = None,\n                 sd_hash: typing.Optional[str] = None):\n        self.loop = loop\n        self.blob_dir = blob_dir\n        self.stream_name = stream_name\n        self.key = key\n        self.suggested_file_name = suggested_file_name\n        self.blobs = blobs\n        self.stream_hash = stream_hash or self.get_stream_hash()\n        self.sd_hash = sd_hash\n\n    @property\n    def length(self) -> int:\n        return len(self.as_json())\n\n    def get_stream_hash(self) -> str:\n        return self.calculate_stream_hash(\n            binascii.hexlify(self.stream_name.encode()), self.key.encode(),\n            binascii.hexlify(self.suggested_file_name.encode()),\n            [blob_info.as_dict() for blob_info in self.blobs]\n        )\n\n    def calculate_sd_hash(self) -> str:\n        h = get_lbry_hash_obj()\n        h.update(self.as_json())\n        return h.hexdigest()\n\n    def as_json(self) -> bytes:\n        return json.dumps(\n            format_sd_info(binascii.hexlify(self.stream_name.encode()).decode(), self.key,\n                           binascii.hexlify(self.suggested_file_name.encode()).decode(),\n                           self.stream_hash,\n                           [blob_info.as_dict() for blob_info in self.blobs]), sort_keys=True\n        ).encode()\n\n    def old_sort_json(self) -> bytes:\n        blobs = []\n        for blob in self.blobs:\n            blobs.append(OrderedDict(\n                [('length', blob.length), ('blob_num', blob.blob_num), ('iv', blob.iv)] if not blob.blob_hash else\n                [('length', blob.length), ('blob_num', blob.blob_num), ('blob_hash', blob.blob_hash), ('iv', blob.iv)]\n            ))\n            if not blob.blob_hash:\n                break\n        return json.dumps(\n            OrderedDict([\n                ('stream_name', binascii.hexlify(self.stream_name.encode()).decode()),\n                ('blobs', blobs),\n                ('stream_type', 'lbryfile'),\n                ('key', self.key),\n                ('suggested_file_name', binascii.hexlify(self.suggested_file_name.encode()).decode()),\n                ('stream_hash', self.stream_hash),\n            ])\n        ).encode()\n\n    def calculate_old_sort_sd_hash(self) -> str:\n        h = get_lbry_hash_obj()\n        h.update(self.old_sort_json())\n        return h.hexdigest()\n\n    async def make_sd_blob(\n            self, blob_file_obj: typing.Optional[AbstractBlob] = None, old_sort: typing.Optional[bool] = False,\n            blob_completed_callback: typing.Optional[typing.Callable[['AbstractBlob'], None]] = None,\n            added_on: float = None, is_mine: bool = False\n        ):\n        sd_hash = self.calculate_sd_hash() if not old_sort else self.calculate_old_sort_sd_hash()\n        if not old_sort:\n            sd_data = self.as_json()\n        else:\n            sd_data = self.old_sort_json()\n        sd_blob = blob_file_obj or BlobFile(\n            self.loop, sd_hash, len(sd_data), blob_completed_callback, self.blob_dir, added_on, is_mine\n        )\n        if blob_file_obj:\n            blob_file_obj.set_length(len(sd_data))\n        if not sd_blob.get_is_verified():\n            writer = sd_blob.get_blob_writer()\n            writer.write(sd_data)\n\n        await sd_blob.verified.wait()\n        sd_blob.close()\n        return sd_blob\n\n    @classmethod\n    def _from_stream_descriptor_blob(cls, loop: asyncio.AbstractEventLoop, blob_dir: str,\n                                     blob: AbstractBlob) -> 'StreamDescriptor':\n        with blob.reader_context() as blob_reader:\n            json_bytes = blob_reader.read()\n        try:\n            decoded = json.loads(json_bytes.decode())\n        except json.JSONDecodeError:\n            blob.delete()\n            raise InvalidStreamDescriptorError(\"Does not decode as valid JSON\")\n        if decoded['blobs'][-1]['length'] != 0:\n            raise InvalidStreamDescriptorError(\"Does not end with a zero-length blob.\")\n        if any(blob_info['length'] == 0 for blob_info in decoded['blobs'][:-1]):\n            raise InvalidStreamDescriptorError(\"Contains zero-length data blob\")\n        if 'blob_hash' in decoded['blobs'][-1]:\n            raise InvalidStreamDescriptorError(\"Stream terminator blob should not have a hash\")\n        if any(i != blob_info['blob_num'] for i, blob_info in enumerate(decoded['blobs'])):\n            raise InvalidStreamDescriptorError(\"Stream contains out of order or skipped blobs\")\n        added_on = time.time()\n        descriptor = cls(\n            loop, blob_dir,\n            binascii.unhexlify(decoded['stream_name']).decode(),\n            decoded['key'],\n            binascii.unhexlify(decoded['suggested_file_name']).decode(),\n            [BlobInfo(info['blob_num'], info['length'], info['iv'], added_on, info.get('blob_hash'))\n             for info in decoded['blobs']],\n            decoded['stream_hash'],\n            blob.blob_hash\n        )\n        if descriptor.get_stream_hash() != decoded['stream_hash']:\n            raise InvalidStreamDescriptorError(\"Stream hash does not match stream metadata\")\n        return descriptor\n\n    @classmethod\n    async def from_stream_descriptor_blob(cls, loop: asyncio.AbstractEventLoop, blob_dir: str,\n                                          blob: AbstractBlob) -> 'StreamDescriptor':\n        if not blob.is_readable():\n            raise InvalidStreamDescriptorError(f\"unreadable/missing blob: {blob.blob_hash}\")\n        return await loop.run_in_executor(None, cls._from_stream_descriptor_blob, loop, blob_dir, blob)\n\n    @staticmethod\n    def get_blob_hashsum(blob_dict: typing.Dict):\n        length = blob_dict['length']\n        if length != 0:\n            blob_hash = blob_dict['blob_hash']\n        else:\n            blob_hash = None\n        blob_num = blob_dict['blob_num']\n        iv = blob_dict['iv']\n        blob_hashsum = get_lbry_hash_obj()\n        if length != 0:\n            blob_hashsum.update(blob_hash.encode())\n        blob_hashsum.update(str(blob_num).encode())\n        blob_hashsum.update(iv.encode())\n        blob_hashsum.update(str(length).encode())\n        return blob_hashsum.digest()\n\n    @staticmethod\n    def calculate_stream_hash(hex_stream_name: bytes, key: bytes, hex_suggested_file_name: bytes,\n                              blob_infos: typing.List[typing.Dict]) -> str:\n        h = get_lbry_hash_obj()\n        h.update(hex_stream_name)\n        h.update(key)\n        h.update(hex_suggested_file_name)\n        blobs_hashsum = get_lbry_hash_obj()\n        for blob in blob_infos:\n            blobs_hashsum.update(StreamDescriptor.get_blob_hashsum(blob))\n        h.update(blobs_hashsum.digest())\n        return h.hexdigest()\n\n    @classmethod\n    async def create_stream(\n            cls, loop: asyncio.AbstractEventLoop, blob_dir: str, file_path: str, key: typing.Optional[bytes] = None,\n            iv_generator: typing.Optional[typing.Generator[bytes, None, None]] = None,\n            old_sort: bool = False,\n            blob_completed_callback: typing.Optional[typing.Callable[['AbstractBlob'],\n                                                                     asyncio.Task]] = None) -> 'StreamDescriptor':\n        blobs: typing.List[BlobInfo] = []\n\n        iv_generator = iv_generator or random_iv_generator()\n        key = key or os.urandom(AES.block_size // 8)\n        blob_num = -1\n        added_on = time.time()\n        async for blob_bytes in file_reader(file_path):\n            blob_num += 1\n            blob_info = await BlobFile.create_from_unencrypted(\n                loop, blob_dir, key, next(iv_generator), blob_bytes, blob_num, added_on, True, blob_completed_callback\n            )\n            blobs.append(blob_info)\n        blobs.append(\n            # add the stream terminator\n            BlobInfo(len(blobs), 0, binascii.hexlify(next(iv_generator)).decode(), added_on, None, True)\n        )\n        file_name = os.path.basename(file_path)\n        suggested_file_name = sanitize_file_name(file_name)\n        descriptor = cls(\n            loop, blob_dir, file_name, binascii.hexlify(key).decode(), suggested_file_name, blobs\n        )\n        sd_blob = await descriptor.make_sd_blob(\n            old_sort=old_sort, blob_completed_callback=blob_completed_callback, added_on=added_on, is_mine=True\n        )\n        descriptor.sd_hash = sd_blob.blob_hash\n        return descriptor\n\n    def lower_bound_decrypted_length(self) -> int:\n        length = sum(blob.length - 1 for blob in self.blobs[:-2])\n        return length + self.blobs[-2].length - (AES.block_size // 8)\n\n    def upper_bound_decrypted_length(self) -> int:\n        return self.lower_bound_decrypted_length() + (AES.block_size // 8)\n\n    @classmethod\n    async def recover(cls, blob_dir: str, sd_blob: 'AbstractBlob', stream_hash: str, stream_name: str,\n                      suggested_file_name: str, key: str,\n                      blobs: typing.List['BlobInfo']) -> typing.Optional['StreamDescriptor']:\n        descriptor = cls(asyncio.get_event_loop(), blob_dir, stream_name, key, suggested_file_name,\n                         blobs, stream_hash, sd_blob.blob_hash)\n\n        if descriptor.calculate_sd_hash() == sd_blob.blob_hash:  # first check for a normal valid sd\n            old_sort = False\n        elif descriptor.calculate_old_sort_sd_hash() == sd_blob.blob_hash:  # check if old field sorting works\n            old_sort = True\n        else:\n            return\n        await descriptor.make_sd_blob(sd_blob, old_sort)\n        return descriptor\n"
  },
  {
    "path": "lbry/stream/downloader.py",
    "content": "import asyncio\nimport typing\nimport logging\nimport binascii\n\nfrom lbry.dht.node import get_kademlia_peers_from_hosts\nfrom lbry.error import DownloadSDTimeoutError\nfrom lbry.utils import lru_cache_concurrent\nfrom lbry.stream.descriptor import StreamDescriptor\nfrom lbry.blob_exchange.downloader import BlobDownloader\nfrom lbry.torrent.tracker import enqueue_tracker_search\n\nif typing.TYPE_CHECKING:\n    from lbry.conf import Config\n    from lbry.dht.node import Node\n    from lbry.blob.blob_manager import BlobManager\n    from lbry.blob.blob_file import AbstractBlob\n    from lbry.blob.blob_info import BlobInfo\n\nlog = logging.getLogger(__name__)\n\n\nclass StreamDownloader:\n    def __init__(self, loop: asyncio.AbstractEventLoop, config: 'Config', blob_manager: 'BlobManager', sd_hash: str,\n                 descriptor: typing.Optional[StreamDescriptor] = None):\n        self.loop = loop\n        self.config = config\n        self.blob_manager = blob_manager\n        self.sd_hash = sd_hash\n        self.search_queue = asyncio.Queue()     # blob hashes to feed into the iterative finder\n        self.peer_queue = asyncio.Queue()       # new peers to try\n        self.blob_downloader = BlobDownloader(self.loop, self.config, self.blob_manager, self.peer_queue)\n        self.descriptor: typing.Optional[StreamDescriptor] = descriptor\n        self.node: typing.Optional['Node'] = None\n        self.accumulate_task: typing.Optional[asyncio.Task] = None\n        self.fixed_peers_handle: typing.Optional[asyncio.Handle] = None\n        self.fixed_peers_delay: typing.Optional[float] = None\n        self.added_fixed_peers = False\n        self.time_to_descriptor: typing.Optional[float] = None\n        self.time_to_first_bytes: typing.Optional[float] = None\n\n        async def cached_read_blob(blob_info: 'BlobInfo') -> bytes:\n            return await self.read_blob(blob_info, 2)\n\n        if self.blob_manager.decrypted_blob_lru_cache is not None:\n            cached_read_blob = lru_cache_concurrent(override_lru_cache=self.blob_manager.decrypted_blob_lru_cache)(\n                cached_read_blob\n            )\n\n        self.cached_read_blob = cached_read_blob\n\n    async def add_fixed_peers(self):\n        def _add_fixed_peers(fixed_peers):\n            self.peer_queue.put_nowait(fixed_peers)\n            self.added_fixed_peers = True\n\n        if not self.config.fixed_peers:\n            return\n        if 'dht' in self.config.components_to_skip or not self.node or not \\\n                len(self.node.protocol.routing_table.get_peers()) > 0:\n            self.fixed_peers_delay = 0.0\n        else:\n            self.fixed_peers_delay = self.config.fixed_peer_delay\n        fixed_peers = await get_kademlia_peers_from_hosts(self.config.fixed_peers)\n        self.fixed_peers_handle = self.loop.call_later(self.fixed_peers_delay, _add_fixed_peers, fixed_peers)\n\n    async def load_descriptor(self, connection_id: int = 0):\n        # download or get the sd blob\n        sd_blob = self.blob_manager.get_blob(self.sd_hash)\n        if not sd_blob.get_is_verified():\n            try:\n                now = self.loop.time()\n                sd_blob = await asyncio.wait_for(\n                    self.blob_downloader.download_blob(self.sd_hash, connection_id),\n                    self.config.blob_download_timeout\n                )\n                log.info(\"downloaded sd blob %s\", self.sd_hash)\n                self.time_to_descriptor = self.loop.time() - now\n            except asyncio.TimeoutError:\n                raise DownloadSDTimeoutError(self.sd_hash)\n\n        # parse the descriptor\n        self.descriptor = await StreamDescriptor.from_stream_descriptor_blob(\n            self.loop, self.blob_manager.blob_dir, sd_blob\n        )\n        log.info(\"loaded stream manifest %s\", self.sd_hash)\n\n    async def start(self, node: typing.Optional['Node'] = None, connection_id: int = 0, save_stream=True):\n        # set up peer accumulation\n        self.node = node or self.node  # fixme: this shouldnt be set here!\n        if self.node:\n            if self.accumulate_task and not self.accumulate_task.done():\n                self.accumulate_task.cancel()\n            _, self.accumulate_task = self.node.accumulate_peers(self.search_queue, self.peer_queue)\n        await self.add_fixed_peers()\n        enqueue_tracker_search(bytes.fromhex(self.sd_hash), self.peer_queue)\n        # start searching for peers for the sd hash\n        self.search_queue.put_nowait(self.sd_hash)\n        log.info(\"searching for peers for stream %s\", self.sd_hash)\n\n        if not self.descriptor:\n            await self.load_descriptor(connection_id)\n\n        if not await self.blob_manager.storage.stream_exists(self.sd_hash) and save_stream:\n            await self.blob_manager.storage.store_stream(\n                self.blob_manager.get_blob(self.sd_hash, length=self.descriptor.length), self.descriptor\n            )\n\n    async def download_stream_blob(self, blob_info: 'BlobInfo', connection_id: int = 0) -> 'AbstractBlob':\n        if not filter(lambda b: b.blob_hash == blob_info.blob_hash, self.descriptor.blobs[:-1]):\n            raise ValueError(f\"blob {blob_info.blob_hash} is not part of stream with sd hash {self.sd_hash}\")\n        blob = await asyncio.wait_for(\n            self.blob_downloader.download_blob(blob_info.blob_hash, blob_info.length, connection_id),\n            self.config.blob_download_timeout * 10\n        )\n        return blob\n\n    def decrypt_blob(self, blob_info: 'BlobInfo', blob: 'AbstractBlob') -> bytes:\n        return blob.decrypt(\n            binascii.unhexlify(self.descriptor.key.encode()), binascii.unhexlify(blob_info.iv.encode())\n        )\n\n    async def read_blob(self, blob_info: 'BlobInfo', connection_id: int = 0) -> bytes:\n        start = None\n        if self.time_to_first_bytes is None:\n            start = self.loop.time()\n        blob = await self.download_stream_blob(blob_info, connection_id)\n        decrypted = self.decrypt_blob(blob_info, blob)\n        if start:\n            self.time_to_first_bytes = self.loop.time() - start\n        return decrypted\n\n    def stop(self):\n        if self.accumulate_task:\n            self.accumulate_task.cancel()\n            self.accumulate_task = None\n        if self.fixed_peers_handle:\n            self.fixed_peers_handle.cancel()\n            self.fixed_peers_handle = None\n        self.blob_downloader.close()\n"
  },
  {
    "path": "lbry/stream/managed_stream.py",
    "content": "import os\nimport asyncio\nimport time\nimport typing\nimport logging\nfrom typing import Optional\nfrom aiohttp.web import Request, StreamResponse, HTTPRequestRangeNotSatisfiable\nfrom lbry.error import DownloadSDTimeoutError\nfrom lbry.schema.mime_types import guess_media_type\nfrom lbry.stream.downloader import StreamDownloader\nfrom lbry.stream.descriptor import StreamDescriptor, sanitize_file_name\nfrom lbry.stream.reflector.client import StreamReflectorClient\nfrom lbry.extras.daemon.storage import StoredContentClaim\nfrom lbry.blob import MAX_BLOB_SIZE\nfrom lbry.file.source import ManagedDownloadSource\n\nif typing.TYPE_CHECKING:\n    from lbry.conf import Config\n    from lbry.blob.blob_manager import BlobManager\n    from lbry.blob.blob_info import BlobInfo\n    from lbry.extras.daemon.analytics import AnalyticsManager\n    from lbry.wallet.transaction import Transaction\n\nlog = logging.getLogger(__name__)\n\n\ndef _get_next_available_file_name(download_directory: str, file_name: str) -> str:\n    base_name, ext = os.path.splitext(os.path.basename(file_name))\n    i = 0\n    while os.path.isfile(os.path.join(download_directory, file_name)):\n        i += 1\n        file_name = \"%s_%i%s\" % (base_name, i, ext)\n\n    return file_name\n\n\nasync def get_next_available_file_name(loop: asyncio.AbstractEventLoop, download_directory: str, file_name: str) -> str:\n    return await loop.run_in_executor(None, _get_next_available_file_name, download_directory, file_name)\n\n\nclass ManagedStream(ManagedDownloadSource):\n    def __init__(self, loop: asyncio.AbstractEventLoop, config: 'Config', blob_manager: 'BlobManager',\n                 sd_hash: str, download_directory: Optional[str] = None, file_name: Optional[str] = None,\n                 status: Optional[str] = ManagedDownloadSource.STATUS_STOPPED,\n                 claim: Optional[StoredContentClaim] = None,\n                 download_id: Optional[str] = None, rowid: Optional[int] = None,\n                 descriptor: Optional[StreamDescriptor] = None,\n                 content_fee: Optional['Transaction'] = None,\n                 analytics_manager: Optional['AnalyticsManager'] = None,\n                 added_on: Optional[int] = None):\n        super().__init__(loop, config, blob_manager.storage, sd_hash, file_name, download_directory, status, claim,\n                         download_id, rowid, content_fee, analytics_manager, added_on)\n        self.blob_manager = blob_manager\n        self.purchase_receipt = None\n        self.downloader = StreamDownloader(self.loop, self.config, self.blob_manager, sd_hash, descriptor)\n        self.analytics_manager = analytics_manager\n\n        self.reflector_progress = 0\n        self.uploading_to_reflector = False\n        self.file_output_task: typing.Optional[asyncio.Task] = None\n        self.delayed_stop_task: typing.Optional[asyncio.Task] = None\n        self.streaming_responses: typing.List[typing.Tuple[Request, StreamResponse]] = []\n        self.fully_reflected = asyncio.Event()\n        self.streaming = asyncio.Event()\n        self._running = asyncio.Event()\n\n    @property\n    def sd_hash(self) -> str:\n        return self.identifier\n\n    @property\n    def is_fully_reflected(self) -> bool:\n        return self.fully_reflected.is_set()\n\n    @property\n    def descriptor(self) -> StreamDescriptor:\n        return self.downloader.descriptor\n\n    @property\n    def stream_hash(self) -> str:\n        return self.descriptor.stream_hash\n\n    @property\n    def file_name(self) -> Optional[str]:\n        return self._file_name or self.suggested_file_name\n\n    @property\n    def suggested_file_name(self) -> Optional[str]:\n        first_option = ((self.descriptor and self.descriptor.suggested_file_name) or '').strip()\n        return sanitize_file_name(first_option or (self.stream_claim_info and self.stream_claim_info.claim and\n                                                   self.stream_claim_info.claim.stream.source.name))\n\n    @property\n    def stream_name(self) -> Optional[str]:\n        first_option = ((self.descriptor and self.descriptor.stream_name) or '').strip()\n        return first_option or (self.stream_claim_info and self.stream_claim_info.claim and\n                                self.stream_claim_info.claim.stream.source.name)\n\n    @property\n    def written_bytes(self) -> int:\n        return 0 if not self.output_file_exists else os.stat(self.full_path).st_size\n\n    @property\n    def completed(self):\n        return self.written_bytes >= self.descriptor.lower_bound_decrypted_length()\n\n    @property\n    def stream_url(self):\n        return f\"http://{self.config.streaming_host}:{self.config.streaming_port}/stream/{self.sd_hash}\"\n\n    async def update_status(self, status: str):\n        assert status in [self.STATUS_RUNNING, self.STATUS_STOPPED, self.STATUS_FINISHED]\n        self._status = status\n        await self.blob_manager.storage.change_file_status(self.stream_hash, status)\n\n    @property\n    def blobs_completed(self) -> int:\n        return sum([1 if b.blob_hash in self.blob_manager.completed_blob_hashes else 0\n                    for b in self.descriptor.blobs[:-1]])\n\n    @property\n    def blobs_in_stream(self) -> int:\n        return len(self.descriptor.blobs) - 1\n\n    @property\n    def blobs_remaining(self) -> int:\n        return self.blobs_in_stream - self.blobs_completed\n\n    @property\n    def mime_type(self):\n        return guess_media_type(os.path.basename(self.suggested_file_name))[0]\n\n    @property\n    def download_path(self):\n        return f\"{self.download_directory}/{self._file_name}\" if self.download_directory and self._file_name else None\n\n    # @classmethod\n    # async def create(cls, loop: asyncio.AbstractEventLoop, config: 'Config',\n    #                  file_path: str, key: Optional[bytes] = None,\n    #                  iv_generator: Optional[typing.Generator[bytes, None, None]] = None) -> 'ManagedDownloadSource':\n    #     \"\"\"\n    #     Generate a stream from a file and save it to the db\n    #     \"\"\"\n    #     descriptor = await StreamDescriptor.create_stream(\n    #         loop, blob_manager.blob_dir, file_path, key=key, iv_generator=iv_generator,\n    #         blob_completed_callback=blob_manager.blob_completed\n    #     )\n    #     await blob_manager.storage.store_stream(\n    #         blob_manager.get_blob(descriptor.sd_hash), descriptor\n    #     )\n    #     row_id = await blob_manager.storage.save_published_file(descriptor.stream_hash, os.path.basename(file_path),\n    #                                                             os.path.dirname(file_path), 0)\n    #     return cls(loop, config, blob_manager, descriptor.sd_hash, os.path.dirname(file_path),\n    #                os.path.basename(file_path), status=cls.STATUS_FINISHED, rowid=row_id, descriptor=descriptor)\n\n    async def start(self, timeout: Optional[float] = None,\n                    save_now: bool = False):\n        timeout = timeout or self.config.download_timeout\n        if self._running.is_set():\n            return\n        log.info(\"start downloader for stream (sd hash: %s)\", self.sd_hash)\n        self._running.set()\n        try:\n            await asyncio.wait_for(self.downloader.start(), timeout)\n        except asyncio.TimeoutError:\n            self._running.clear()\n            raise DownloadSDTimeoutError(self.sd_hash)\n\n        if self.delayed_stop_task and not self.delayed_stop_task.done():\n            self.delayed_stop_task.cancel()\n        self.delayed_stop_task = self.loop.create_task(self._delayed_stop())\n        if not await self.blob_manager.storage.file_exists(self.sd_hash):\n            if save_now:\n                if not self._file_name:\n                    self._file_name = await get_next_available_file_name(\n                        self.loop, self.download_directory,\n                        self._file_name or sanitize_file_name(self.suggested_file_name)\n                    )\n                file_name, download_dir = self._file_name, self.download_directory\n            else:\n                file_name, download_dir = None, None\n            self._added_on = int(time.time())\n            self.rowid = await self.blob_manager.storage.save_downloaded_file(\n                self.stream_hash, file_name, download_dir, 0.0, added_on=self._added_on\n            )\n        if self.status != self.STATUS_RUNNING:\n            await self.update_status(self.STATUS_RUNNING)\n\n    async def stop(self, finished: bool = False):\n        \"\"\"\n        Stop any running save/stream tasks as well as the downloader and update the status in the database\n        \"\"\"\n\n        await self.stop_tasks()\n        if (finished and self.status != self.STATUS_FINISHED) or self.status == self.STATUS_RUNNING:\n            await self.update_status(self.STATUS_FINISHED if finished else self.STATUS_STOPPED)\n\n    async def _aiter_read_stream(self, start_blob_num: Optional[int] = 0, connection_id: int = 0)\\\n            -> typing.AsyncIterator[typing.Tuple['BlobInfo', bytes]]:\n        if start_blob_num >= len(self.descriptor.blobs[:-1]):\n            raise IndexError(start_blob_num)\n        for i, blob_info in enumerate(self.descriptor.blobs[start_blob_num:-1]):\n            assert i + start_blob_num == blob_info.blob_num\n            if connection_id == self.STREAMING_ID:\n                decrypted = await self.downloader.cached_read_blob(blob_info)\n            else:\n                decrypted = await self.downloader.read_blob(blob_info, connection_id)\n            yield (blob_info, decrypted)\n\n    async def stream_file(self, request: Request) -> StreamResponse:\n        log.info(\"stream file to browser for lbry://%s#%s (sd hash %s...)\", self.claim_name, self.claim_id,\n                 self.sd_hash[:6])\n        headers, size, skip_blobs, first_blob_start_offset = self._prepare_range_response_headers(\n            request.headers.get('range', 'bytes=0-')\n        )\n        await self.start()\n        response = StreamResponse(\n            status=206,\n            headers=headers\n        )\n        await response.prepare(request)\n        self.streaming_responses.append((request, response))\n        self.streaming.set()\n        wrote = 0\n        try:\n            async for blob_info, decrypted in self._aiter_read_stream(skip_blobs, connection_id=self.STREAMING_ID):\n                if not wrote:\n                    decrypted = decrypted[first_blob_start_offset:]\n                if (blob_info.blob_num == len(self.descriptor.blobs) - 2) or (len(decrypted) + wrote >= size):\n                    decrypted += (b'\\x00' * (size - len(decrypted) - wrote - (skip_blobs * (MAX_BLOB_SIZE - 1))))\n                    log.debug(\"sending browser final blob (%i/%i)\", blob_info.blob_num + 1,\n                              len(self.descriptor.blobs) - 1)\n                    await response.write_eof(decrypted)\n                else:\n                    log.debug(\"sending browser blob (%i/%i)\", blob_info.blob_num + 1, len(self.descriptor.blobs) - 1)\n                    await response.write(decrypted)\n                wrote += len(decrypted)\n                log.info(\"sent browser %sblob %i/%i\", \"(final) \" if response._eof_sent else \"\",\n                         blob_info.blob_num + 1, len(self.descriptor.blobs) - 1)\n                if response._eof_sent:\n                    break\n            return response\n        except ConnectionResetError:\n            log.warning(\"connection was reset after sending browser %i blob bytes\", wrote)\n            raise asyncio.CancelledError(\"range request transport was reset\")\n        finally:\n            response.force_close()\n            if (request, response) in self.streaming_responses:\n                self.streaming_responses.remove((request, response))\n            if not self.streaming_responses:\n                self.streaming.clear()\n\n    @staticmethod\n    def _write_decrypted_blob(output_path: str, data: bytes):\n        with open(output_path, 'ab') as handle:\n            handle.write(data)\n            handle.flush()\n\n    async def _save_file(self, output_path: str):\n        log.info(\"save file for lbry://%s#%s (sd hash %s...) -> %s\", self.claim_name, self.claim_id, self.sd_hash[:6],\n                 output_path)\n        self.saving.set()\n        self.finished_write_attempt.clear()\n        self.finished_writing.clear()\n        self.started_writing.clear()\n        try:\n            open(output_path, 'wb').close()  # pylint: disable=consider-using-with\n            async for blob_info, decrypted in self._aiter_read_stream(connection_id=self.SAVING_ID):\n                log.info(\"write blob %i/%i\", blob_info.blob_num + 1, len(self.descriptor.blobs) - 1)\n                await self.loop.run_in_executor(None, self._write_decrypted_blob, output_path, decrypted)\n                if not self.started_writing.is_set():\n                    self.started_writing.set()\n            await self.update_status(ManagedStream.STATUS_FINISHED)\n            if self.analytics_manager:\n                self.loop.create_task(self.analytics_manager.send_download_finished(\n                    self.download_id, self.claim_name, self.sd_hash\n                ))\n            self.finished_writing.set()\n            log.info(\"finished saving file for lbry://%s#%s (sd hash %s...) -> %s\", self.claim_name, self.claim_id,\n                     self.sd_hash[:6], self.full_path)\n            await self.blob_manager.storage.set_saved_file(self.stream_hash)\n        except (Exception, asyncio.CancelledError) as err:\n            if os.path.isfile(output_path):\n                log.warning(\"removing incomplete download %s for %s\", output_path, self.sd_hash)\n                os.remove(output_path)\n            if isinstance(err, asyncio.TimeoutError):\n                self.downloader.stop()\n                await self.blob_manager.storage.change_file_download_dir_and_file_name(\n                    self.stream_hash, None, None\n                )\n                self._file_name, self.download_directory = None, None\n                await self.blob_manager.storage.clear_saved_file(self.stream_hash)\n                await self.update_status(self.STATUS_STOPPED)\n                return\n            elif not isinstance(err, asyncio.CancelledError):\n                log.exception(\"unexpected error encountered writing file for stream %s\", self.sd_hash)\n            raise err\n        finally:\n            self.saving.clear()\n            self.finished_write_attempt.set()\n\n    async def save_file(self, file_name: Optional[str] = None, download_directory: Optional[str] = None):\n        await self.start()\n        if self.file_output_task and not self.file_output_task.done():  # cancel an already running save task\n            self.file_output_task.cancel()\n        self.download_directory = download_directory or self.download_directory or self.config.download_dir\n        if not self.download_directory:\n            raise ValueError(\"no directory to download to\")\n        if not (file_name or self._file_name or self.suggested_file_name):\n            raise ValueError(\"no file name to download to\")\n        if not os.path.isdir(self.download_directory):\n            log.warning(\"download directory '%s' does not exist, attempting to make it\", self.download_directory)\n            os.mkdir(self.download_directory)\n        self._file_name = await get_next_available_file_name(\n            self.loop, self.download_directory,\n            file_name or self._file_name or sanitize_file_name(self.suggested_file_name)\n        )\n        await self.blob_manager.storage.change_file_download_dir_and_file_name(\n            self.stream_hash, self.download_directory, self.file_name\n        )\n        await self.update_status(ManagedStream.STATUS_RUNNING)\n        self.file_output_task = self.loop.create_task(self._save_file(self.full_path))\n        try:\n            await asyncio.wait_for(self.started_writing.wait(), self.config.download_timeout)\n        except asyncio.TimeoutError:\n            log.warning(\"timeout starting to write data for lbry://%s#%s\", self.claim_name, self.claim_id)\n            await self.stop_tasks()\n            await self.update_status(ManagedStream.STATUS_STOPPED)\n\n    async def stop_tasks(self):\n        if self.file_output_task and not self.file_output_task.done():\n            self.file_output_task.cancel()\n            await asyncio.gather(self.file_output_task, return_exceptions=True)\n        self.file_output_task = None\n        while self.streaming_responses:\n            req, response = self.streaming_responses.pop()\n            response.force_close()\n            req.transport.close()\n        self.downloader.stop()\n        self._running.clear()\n\n    async def upload_to_reflector(self, host: str, port: int) -> typing.List[str]:\n        sent = []\n        protocol = StreamReflectorClient(self.blob_manager, self.descriptor)\n        try:\n            self.uploading_to_reflector = True\n            await self.loop.create_connection(lambda: protocol, host, port)\n            await protocol.send_handshake()\n            sent_sd, needed = await protocol.send_descriptor()\n            if sent_sd:  # reflector needed the sd blob\n                sent.append(self.sd_hash)\n            if not sent_sd and not needed:  # reflector already has the stream\n                if not self.fully_reflected.is_set():\n                    self.fully_reflected.set()\n                    await self.blob_manager.storage.update_reflected_stream(self.sd_hash, f\"{host}:{port}\")\n                    return []\n            we_have = [\n                blob_hash for blob_hash in needed if blob_hash in self.blob_manager.completed_blob_hashes\n            ]\n            log.info(\"we have %i/%i needed blobs needed by reflector for lbry://%s#%s\", len(we_have), len(needed),\n                     self.claim_name, self.claim_id)\n            for i, blob_hash in enumerate(we_have):\n                await protocol.send_blob(blob_hash)\n                sent.append(blob_hash)\n                self.reflector_progress = int((i + 1) / len(we_have) * 100)\n        except (asyncio.TimeoutError, ValueError):\n            return sent\n        except ConnectionError:\n            return sent\n        except (OSError, Exception, asyncio.CancelledError) as err:\n            if isinstance(err, asyncio.CancelledError):\n                log.warning(\"stopped uploading %s#%s to reflector\", self.claim_name, self.claim_id)\n            elif isinstance(err, OSError):\n                log.warning(\n                    \"stopped uploading %s#%s to reflector because blobs were deleted or moved\", self.claim_name,\n                    self.claim_id\n                )\n            else:\n                log.exception(\"unexpected error reflecting %s#%s\", self.claim_name, self.claim_id)\n            return sent\n        finally:\n            if protocol.transport:\n                protocol.transport.close()\n            self.uploading_to_reflector = False\n\n        return sent\n\n    async def update_content_claim(self, claim_info: Optional[typing.Dict] = None):\n        if not claim_info:\n            claim_info = await self.blob_manager.storage.get_content_claim(self.stream_hash)\n        self.set_claim(claim_info, claim_info['value'])\n\n    async def _delayed_stop(self):\n        stalled_count = 0\n        while self._running.is_set():\n            if self.saving.is_set() or self.streaming.is_set():\n                stalled_count = 0\n            else:\n                stalled_count += 1\n            if stalled_count > 1:\n                log.info(\"stopping inactive download for lbry://%s#%s (%s...)\", self.claim_name, self.claim_id,\n                         self.sd_hash[:6])\n                await self.stop()\n                return\n            await asyncio.sleep(1)\n\n    def _prepare_range_response_headers(self, get_range: str) -> typing.Tuple[typing.Dict[str, str], int, int, int]:\n        if '=' in get_range:\n            get_range = get_range.split('=')[1]\n        start, end = get_range.split('-')\n        size = 0\n\n        for blob in self.descriptor.blobs[:-1]:\n            size += blob.length - 1\n        if self.stream_claim_info and self.stream_claim_info.claim.stream.source.size:\n            size_from_claim = int(self.stream_claim_info.claim.stream.source.size)\n            if not size_from_claim <= size <= size_from_claim + 16:\n                raise ValueError(\"claim contains implausible stream size\")\n            log.debug(\"using stream size from claim\")\n            size = size_from_claim\n        elif self.stream_claim_info:\n            log.debug(\"estimating stream size\")\n\n        start = int(start)\n        if not 0 <= start < size:\n            raise HTTPRequestRangeNotSatisfiable()\n\n        end = int(end) if end else size - 1\n\n        if end >= size:\n            raise HTTPRequestRangeNotSatisfiable()\n\n        skip_blobs = start // (MAX_BLOB_SIZE - 2)  # -2 because ... dont remember\n        skip = skip_blobs * (MAX_BLOB_SIZE - 1)  # -1 because\n        skip_first_blob = start - skip\n        start = skip_first_blob + skip\n        final_size = end - start + 1\n        headers = {\n            'Accept-Ranges': 'bytes',\n            'Content-Range': f'bytes {start}-{end}/{size}',\n            'Content-Length': str(final_size),\n            'Content-Type': self.mime_type\n        }\n        return headers, size, skip_blobs, skip_first_blob\n"
  },
  {
    "path": "lbry/stream/reflector/__init__.py",
    "content": ""
  },
  {
    "path": "lbry/stream/reflector/client.py",
    "content": "import asyncio\nimport json\nimport logging\nimport typing\n\nif typing.TYPE_CHECKING:\n    from lbry.blob.blob_manager import BlobManager\n    from lbry.stream.descriptor import StreamDescriptor\n\nREFLECTOR_V1 = 0\nREFLECTOR_V2 = 1\n\nMAX_RESPONSE_SIZE = 2000000\n\nlog = logging.getLogger(__name__)\n\n\nclass StreamReflectorClient(asyncio.Protocol):\n    def __init__(self, blob_manager: 'BlobManager', descriptor: 'StreamDescriptor'):\n        self.loop = asyncio.get_event_loop()\n        self.transport: typing.Optional[asyncio.WriteTransport] = None\n        self.blob_manager = blob_manager\n        self.descriptor = descriptor\n        self.response_buff = b''\n        self.reflected_blobs = []\n        self.connected = asyncio.Event()\n        self.response_queue = asyncio.Queue(maxsize=1)\n        self.pending_request: typing.Optional[asyncio.Task] = None\n\n    def connection_made(self, transport):\n        self.transport = transport\n        log.debug(\"Connected to reflector\")\n        self.connected.set()\n\n    def connection_lost(self, exc: typing.Optional[Exception]):\n        self.transport = None\n        self.connected.clear()\n        if self.pending_request:\n            self.pending_request.cancel()\n        if self.reflected_blobs:\n            log.info(\"Finished sending reflector %i blobs\", len(self.reflected_blobs))\n\n    def data_received(self, data):\n        if len(self.response_buff + (data or b'')) > MAX_RESPONSE_SIZE:\n            log.warning(\"response message to large from reflector server: %i bytes\",\n                        len(self.response_buff + (data or b'')))\n            self.response_buff = b''\n            self.transport.close()\n            return\n        self.response_buff += (data or b'')\n        try:\n            response = json.loads(self.response_buff.decode())\n            self.response_buff = b''\n            self.response_queue.put_nowait(response)\n        except ValueError:\n            if not data:\n                log.warning(\"got undecodable response from reflector server\")\n                self.response_buff = b''\n            return\n\n    async def send_request(self, request_dict: typing.Dict, timeout: int = 180):\n        msg = json.dumps(request_dict, sort_keys=True)\n        try:\n            self.transport.write(msg.encode())\n            self.pending_request = self.loop.create_task(asyncio.wait_for(self.response_queue.get(), timeout))\n            return await self.pending_request\n        except (AttributeError, asyncio.CancelledError) as err:\n            # attribute error happens when we transport.write after disconnect\n            # cancelled error happens when the pending_request task is cancelled by a disconnect\n            if self.transport:\n                self.transport.close()\n            raise err if isinstance(err, asyncio.CancelledError) else asyncio.CancelledError()\n        finally:\n            self.pending_request = None\n\n    async def send_handshake(self) -> None:\n        response_dict = await self.send_request({'version': REFLECTOR_V2})\n        if 'version' not in response_dict:\n            raise ValueError(\"Need protocol version number!\")\n        server_version = int(response_dict['version'])\n        if server_version != REFLECTOR_V2:\n            raise ValueError(f\"I can't handle protocol version {server_version}!\")\n        return\n\n    async def send_descriptor(self) -> typing.Tuple[bool, typing.List[str]]:  # returns a list of needed blob hashes\n        sd_blob = self.blob_manager.get_blob(self.descriptor.sd_hash)\n        assert self.blob_manager.is_blob_verified(self.descriptor.sd_hash), \"need to have sd blob to send at this point\"\n        response = await self.send_request({\n            'sd_blob_hash': sd_blob.blob_hash,\n            'sd_blob_size': sd_blob.length\n        })\n        if 'send_sd_blob' not in response:\n            raise ValueError(\"I don't know whether to send the sd blob or not!\")\n        needed = response.get('needed_blobs', [])\n        sent_sd = False\n        if response['send_sd_blob']:\n            try:\n                sent = await sd_blob.sendfile(self)\n                if sent == -1:\n                    log.warning(\"failed to send sd blob\")\n                    raise asyncio.CancelledError()\n                received = await asyncio.wait_for(self.response_queue.get(), 30)\n            except asyncio.CancelledError as err:\n                if self.transport:\n                    self.transport.close()\n                raise err\n            if received.get('received_sd_blob'):\n                sent_sd = True\n                if not needed:\n                    for blob in self.descriptor.blobs[:-1]:\n                        if self.blob_manager.is_blob_verified(blob.blob_hash, blob.length):\n                            needed.append(blob.blob_hash)\n                log.info(\"Sent reflector descriptor %s\", sd_blob.blob_hash[:8])\n                self.reflected_blobs.append(sd_blob.blob_hash)\n            else:\n                log.warning(\"Reflector failed to receive descriptor %s\", sd_blob.blob_hash[:8])\n        return sent_sd, needed\n\n    async def send_blob(self, blob_hash: str):\n        assert self.blob_manager.is_blob_verified(blob_hash), \"need to have a blob to send at this point\"\n        blob = self.blob_manager.get_blob(blob_hash)\n        response = await self.send_request({\n            'blob_hash': blob.blob_hash,\n            'blob_size': blob.length\n        })\n        if 'send_blob' not in response:\n            raise ValueError(\"I don't know whether to send the blob or not!\")\n        if response['send_blob']:\n            try:\n                sent = await blob.sendfile(self)\n                if sent == -1:\n                    log.warning(\"failed to send blob\")\n                    raise asyncio.CancelledError()\n                received = await asyncio.wait_for(self.response_queue.get(), 30)\n            except asyncio.CancelledError as err:\n                if self.transport:\n                    self.transport.close()\n                raise err\n            if received.get('received_blob'):\n                self.reflected_blobs.append(blob.blob_hash)\n                log.info(\"Sent reflector blob %s\", blob.blob_hash[:8])\n            else:\n                log.warning(\"Reflector failed to receive blob %s\", blob.blob_hash[:8])\n"
  },
  {
    "path": "lbry/stream/reflector/server.py",
    "content": "import asyncio\nimport logging\nimport typing\nimport json\nfrom json.decoder import JSONDecodeError\nfrom lbry.stream.descriptor import StreamDescriptor\n\nif typing.TYPE_CHECKING:\n    from lbry.blob.blob_file import BlobFile\n    from lbry.blob.blob_manager import BlobManager\n    from lbry.blob.writer import HashBlobWriter\n\n\nlog = logging.getLogger(__name__)\n\n\nclass ReflectorServerProtocol(asyncio.Protocol):\n    def __init__(self, blob_manager: 'BlobManager', response_chunk_size: int = 10000,\n                 stop_event: asyncio.Event = None, incoming_event: asyncio.Event = None,\n                 not_incoming_event: asyncio.Event = None, partial_event: asyncio.Event = None):\n        self.loop = asyncio.get_event_loop()\n        self.blob_manager = blob_manager\n        self.server_task: asyncio.Task = None\n        self.started_listening = asyncio.Event()\n        self.buf = b''\n        self.transport: asyncio.StreamWriter = None\n        self.writer: typing.Optional['HashBlobWriter'] = None\n        self.client_version: typing.Optional[int] = None\n        self.descriptor: typing.Optional['StreamDescriptor'] = None\n        self.sd_blob: typing.Optional['BlobFile'] = None\n        self.received = []\n        self.incoming = incoming_event or asyncio.Event()\n        self.not_incoming = not_incoming_event or asyncio.Event()\n        self.stop_event = stop_event or asyncio.Event()\n        self.chunk_size = response_chunk_size\n        self.wait_for_stop_task: typing.Optional[asyncio.Task] = None\n        self.partial_event = partial_event\n\n    async def wait_for_stop(self):\n        await self.stop_event.wait()\n        if self.transport:\n            self.transport.close()\n\n    def connection_made(self, transport):\n        self.transport = transport\n        self.wait_for_stop_task = self.loop.create_task(self.wait_for_stop())\n\n    def connection_lost(self, exc):\n        if self.wait_for_stop_task:\n            self.wait_for_stop_task.cancel()\n            self.wait_for_stop_task = None\n\n    def data_received(self, data: bytes):\n        if self.incoming.is_set():\n            try:\n                self.writer.write(data)\n            except OSError as err:\n                log.error(\"error receiving blob: %s\", err)\n                self.transport.close()\n            return\n        try:\n            request = json.loads(data.decode())\n        except (ValueError, JSONDecodeError):\n            return\n        self.loop.create_task(self.handle_request(request))\n\n    def send_response(self, response: typing.Dict):\n        def chunk_response(remaining: bytes):\n            f = self.loop.create_future()\n            f.add_done_callback(lambda _: self.transport.write(remaining[:self.chunk_size]))\n            if len(remaining) > self.chunk_size:\n                f.add_done_callback(lambda _: self.loop.call_soon(chunk_response, remaining[self.chunk_size:]))\n            self.loop.call_soon(f.set_result, None)\n\n        response_bytes = json.dumps(response).encode()\n        chunk_response(response_bytes)\n\n    async def handle_request(self, request: typing.Dict):  # pylint: disable=too-many-return-statements\n        if self.client_version is None:\n            if 'version' not in request:\n                self.transport.close()\n                return\n            self.client_version = request['version']\n            self.send_response({'version': 1})\n            return\n        if not self.sd_blob:\n            if 'sd_blob_hash' not in request:\n                self.transport.close()\n                return\n            self.sd_blob = self.blob_manager.get_blob(request['sd_blob_hash'], request['sd_blob_size'])\n            if not self.sd_blob.get_is_verified():\n                self.writer = self.sd_blob.get_blob_writer(self.transport.get_extra_info('peername'))\n                self.not_incoming.clear()\n                self.incoming.set()\n                self.send_response({\"send_sd_blob\": True})\n                try:\n                    await asyncio.wait_for(self.sd_blob.verified.wait(), 30)\n                    self.descriptor = await StreamDescriptor.from_stream_descriptor_blob(\n                        self.loop, self.blob_manager.blob_dir, self.sd_blob\n                    )\n                    self.send_response({\"received_sd_blob\": True})\n                except asyncio.TimeoutError:\n                    self.send_response({\"received_sd_blob\": False})\n                    self.transport.close()\n                finally:\n                    self.incoming.clear()\n                    self.not_incoming.set()\n                    self.writer.close_handle()\n                    self.writer = None\n            else:\n                self.descriptor = await StreamDescriptor.from_stream_descriptor_blob(\n                    self.loop, self.blob_manager.blob_dir, self.sd_blob\n                )\n                self.incoming.clear()\n                self.not_incoming.set()\n                if self.writer:\n                    self.writer.close_handle()\n                    self.writer = None\n\n                needs = [blob.blob_hash\n                         for blob in self.descriptor.blobs[:-1]\n                         if not self.blob_manager.get_blob(blob.blob_hash).get_is_verified()]\n                if needs and not self.partial_event.is_set():\n                    needs = needs[:3]\n                    self.partial_event.set()\n                self.send_response({\"send_sd_blob\": False, 'needed_blobs': needs})\n                return\n            return\n        elif self.descriptor:\n            if 'blob_hash' not in request:\n                self.transport.close()\n                return\n            if request['blob_hash'] not in map(lambda b: b.blob_hash, self.descriptor.blobs[:-1]):\n                self.send_response({\"send_blob\": False})\n                return\n            blob = self.blob_manager.get_blob(request['blob_hash'], request['blob_size'])\n            if not blob.get_is_verified():\n                self.writer = blob.get_blob_writer(self.transport.get_extra_info('peername'))\n                self.not_incoming.clear()\n                self.incoming.set()\n                self.send_response({\"send_blob\": True})\n                try:\n                    await asyncio.wait_for(blob.verified.wait(), 30)\n                    self.send_response({\"received_blob\": True})\n                except asyncio.TimeoutError:\n                    self.send_response({\"received_blob\": False})\n                self.incoming.clear()\n                self.not_incoming.set()\n                self.writer.close_handle()\n                self.writer = None\n            else:\n                self.send_response({\"send_blob\": False})\n            return\n        else:\n            self.transport.close()\n\n\nclass ReflectorServer:\n    def __init__(self, blob_manager: 'BlobManager', response_chunk_size: int = 10000,\n                 stop_event: asyncio.Event = None, incoming_event: asyncio.Event = None,\n                 not_incoming_event: asyncio.Event = None, partial_needs=False):\n        self.loop = asyncio.get_event_loop()\n        self.blob_manager = blob_manager\n        self.server_task: typing.Optional[asyncio.Task] = None\n        self.started_listening = asyncio.Event()\n        self.stopped_listening = asyncio.Event()\n        self.incoming_event = incoming_event or asyncio.Event()\n        self.not_incoming_event = not_incoming_event or asyncio.Event()\n        self.response_chunk_size = response_chunk_size\n        self.stop_event = stop_event\n        self.partial_needs = partial_needs  # for testing cases where it doesn't know what it wants\n\n    def start_server(self, port: int, interface: typing.Optional[str] = '0.0.0.0'):\n        if self.server_task is not None:\n            raise Exception(\"already running\")\n\n        async def _start_server():\n            partial_event = asyncio.Event()\n            if not self.partial_needs:\n                partial_event.set()\n            server = await self.loop.create_server(lambda: ReflectorServerProtocol(\n                self.blob_manager, self.response_chunk_size, self.stop_event, self.incoming_event,\n                self.not_incoming_event, partial_event), interface, port)\n            self.started_listening.set()\n            self.stopped_listening.clear()\n            log.info(\"Reflector server listening on TCP %s:%i\", interface, port)\n            try:\n                async with server:\n                    await server.serve_forever()\n            finally:\n                self.stopped_listening.set()\n\n        self.server_task = self.loop.create_task(_start_server())\n\n    def stop_server(self):\n        if self.server_task:\n            self.server_task.cancel()\n            self.server_task = None\n            log.info(\"Stopped reflector server\")\n"
  },
  {
    "path": "lbry/stream/stream_manager.py",
    "content": "import os\nimport asyncio\nimport binascii\nimport logging\nimport random\nimport typing\nfrom typing import Optional\nfrom aiohttp.web import Request\nfrom lbry.error import InvalidStreamDescriptorError\nfrom lbry.file.source_manager import SourceManager\nfrom lbry.stream.descriptor import StreamDescriptor\nfrom lbry.stream.managed_stream import ManagedStream\nfrom lbry.file.source import ManagedDownloadSource\nif typing.TYPE_CHECKING:\n    from lbry.conf import Config\n    from lbry.blob.blob_manager import BlobManager\n    from lbry.dht.node import Node\n    from lbry.wallet.wallet import WalletManager\n    from lbry.wallet.transaction import Transaction\n    from lbry.extras.daemon.analytics import AnalyticsManager\n    from lbry.extras.daemon.storage import SQLiteStorage, StoredContentClaim\n\nlog = logging.getLogger(__name__)\n\n\ndef path_or_none(encoded_path) -> Optional[str]:\n    if not encoded_path:\n        return\n    return binascii.unhexlify(encoded_path).decode()\n\n\nclass StreamManager(SourceManager):\n    _sources: typing.Dict[str, ManagedStream]\n\n    filter_fields = SourceManager.filter_fields\n    filter_fields.update({\n        'sd_hash',\n        'stream_hash',\n        'full_status',  # TODO: remove\n        'blobs_remaining',\n        'blobs_in_stream',\n        'uploading_to_reflector',\n        'is_fully_reflected'\n    })\n\n    def __init__(self, loop: asyncio.AbstractEventLoop, config: 'Config', blob_manager: 'BlobManager',\n                 wallet_manager: 'WalletManager', storage: 'SQLiteStorage', node: Optional['Node'],\n                 analytics_manager: Optional['AnalyticsManager'] = None):\n        super().__init__(loop, config, storage, analytics_manager)\n        self.blob_manager = blob_manager\n        self.wallet_manager = wallet_manager\n        self.node = node\n        self.resume_saving_task: Optional[asyncio.Task] = None\n        self.re_reflect_task: Optional[asyncio.Task] = None\n        self.update_stream_finished_futs: typing.List[asyncio.Future] = []\n        self.running_reflector_uploads: typing.Dict[str, asyncio.Task] = {}\n        self.started = asyncio.Event()\n\n    @property\n    def streams(self):\n        return self._sources\n\n    def add(self, source: ManagedStream):\n        super().add(source)\n        self.storage.content_claim_callbacks[source.stream_hash] = lambda: self._update_content_claim(source)\n\n    async def _update_content_claim(self, stream: ManagedStream):\n        claim_info = await self.storage.get_content_claim(stream.stream_hash)\n        self._sources.setdefault(stream.sd_hash, stream).set_claim(claim_info, claim_info['value'])\n\n    async def recover_streams(self, file_infos: typing.List[typing.Dict]):\n        to_restore = []\n        to_check = []\n\n        async def recover_stream(sd_hash: str, stream_hash: str, stream_name: str,\n                                 suggested_file_name: str, key: str,\n                                 content_fee: Optional['Transaction']) -> Optional[StreamDescriptor]:\n            sd_blob = self.blob_manager.get_blob(sd_hash)\n            blobs = await self.storage.get_blobs_for_stream(stream_hash)\n            descriptor = await StreamDescriptor.recover(\n                self.blob_manager.blob_dir, sd_blob, stream_hash, stream_name, suggested_file_name, key, blobs\n            )\n            if not descriptor:\n                return\n            to_restore.append((descriptor, sd_blob, content_fee))\n            to_check.extend([sd_blob.blob_hash] + [blob.blob_hash for blob in descriptor.blobs[:-1]])\n\n        await asyncio.gather(*[\n            recover_stream(\n                file_info['sd_hash'], file_info['stream_hash'], binascii.unhexlify(file_info['stream_name']).decode(),\n                binascii.unhexlify(file_info['suggested_file_name']).decode(), file_info['key'],\n                file_info['content_fee']\n            ) for file_info in file_infos\n        ])\n\n        if to_restore:\n            await self.storage.recover_streams(to_restore, self.config.download_dir)\n        if to_check:\n            await self.blob_manager.ensure_completed_blobs_status(to_check)\n\n        # if self.blob_manager._save_blobs:\n        #     log.info(\"Recovered %i/%i attempted streams\", len(to_restore), len(file_infos))\n\n    async def _load_stream(self, rowid: int, sd_hash: str, file_name: Optional[str],\n                           download_directory: Optional[str], status: str,\n                           claim: Optional['StoredContentClaim'], content_fee: Optional['Transaction'],\n                           added_on: Optional[int], fully_reflected: Optional[bool]):\n        try:\n            descriptor = await self.blob_manager.get_stream_descriptor(sd_hash)\n        except InvalidStreamDescriptorError as err:\n            log.warning(\"Failed to start stream for sd %s - %s\", sd_hash, str(err))\n            return\n        stream = ManagedStream(\n            self.loop, self.config, self.blob_manager, descriptor.sd_hash, download_directory, file_name, status,\n            claim, content_fee=content_fee, rowid=rowid, descriptor=descriptor,\n            analytics_manager=self.analytics_manager, added_on=added_on\n        )\n        if fully_reflected:\n            stream.fully_reflected.set()\n        self.add(stream)\n\n    async def initialize_from_database(self):\n        to_recover = []\n        to_start = []\n\n        await self.storage.update_manually_removed_files_since_last_run()\n\n        for file_info in await self.storage.get_all_lbry_files():\n            # if the sd blob is not verified, try to reconstruct it from the database\n            # this could either be because the blob files were deleted manually or save_blobs was not true when\n            # the stream was downloaded\n            if not self.blob_manager.is_blob_verified(file_info['sd_hash']):\n                to_recover.append(file_info)\n            to_start.append(file_info)\n        if to_recover:\n            await self.recover_streams(to_recover)\n\n        log.info(\"Initializing %i files\", len(to_start))\n        to_resume_saving = []\n        add_stream_tasks = []\n        for file_info in to_start:\n            file_name = path_or_none(file_info['file_name'])\n            download_directory = path_or_none(file_info['download_directory'])\n            if file_name and download_directory and not file_info['saved_file'] and file_info['status'] == 'running':\n                to_resume_saving.append((file_name, download_directory, file_info['sd_hash']))\n            add_stream_tasks.append(self.loop.create_task(self._load_stream(\n                file_info['rowid'], file_info['sd_hash'], file_name,\n                download_directory, file_info['status'],\n                file_info['claim'], file_info['content_fee'],\n                file_info['added_on'], file_info['fully_reflected']\n            )))\n        if add_stream_tasks:\n            await asyncio.gather(*add_stream_tasks)\n        log.info(\"Started stream manager with %i files\", len(self._sources))\n        if not self.node:\n            log.info(\"no DHT node given, resuming downloads trusting that we can contact reflector\")\n        if to_resume_saving:\n            log.info(\"Resuming saving %i files\", len(to_resume_saving))\n            self.resume_saving_task = asyncio.ensure_future(asyncio.gather(\n                *(self._sources[sd_hash].save_file(file_name, download_directory)\n                  for (file_name, download_directory, sd_hash) in to_resume_saving),\n            ))\n\n    async def reflect_streams(self):\n        try:\n            return await self._reflect_streams()\n        except Exception:\n            log.exception(\"reflector task encountered an unexpected error!\")\n\n    async def _reflect_streams(self):\n        # todo: those debug statements are temporary for #2987 - remove them if its closed\n        while True:\n            if self.config.reflect_streams and self.config.reflector_servers:\n                log.debug(\"collecting streams to reflect\")\n                sd_hashes = await self.storage.get_streams_to_re_reflect()\n                sd_hashes = [sd for sd in sd_hashes if sd in self._sources]\n                batch = []\n                while sd_hashes:\n                    stream = self.streams[sd_hashes.pop()]\n                    if self.blob_manager.is_blob_verified(stream.sd_hash) and stream.blobs_completed and \\\n                            stream.sd_hash not in self.running_reflector_uploads and not \\\n                            stream.fully_reflected.is_set():\n                        batch.append(self.reflect_stream(stream))\n                    if len(batch) >= self.config.concurrent_reflector_uploads:\n                        log.debug(\"waiting for batch of %s reflecting streams\", len(batch))\n                        await asyncio.gather(*batch)\n                        log.debug(\"done processing %s streams\", len(batch))\n                        batch = []\n                if batch:\n                    log.debug(\"waiting for batch of %s reflecting streams\", len(batch))\n                    await asyncio.gather(*batch)\n                    log.debug(\"done processing %s streams\", len(batch))\n            await asyncio.sleep(300)\n\n    async def start(self):\n        await super().start()\n        self.re_reflect_task = self.loop.create_task(self.reflect_streams())\n\n    async def stop(self):\n        await super().stop()\n        if self.resume_saving_task and not self.resume_saving_task.done():\n            self.resume_saving_task.cancel()\n        if self.re_reflect_task and not self.re_reflect_task.done():\n            self.re_reflect_task.cancel()\n        while self.update_stream_finished_futs:\n            self.update_stream_finished_futs.pop().cancel()\n        while self.running_reflector_uploads:\n            _, t = self.running_reflector_uploads.popitem()\n            t.cancel()\n        self.started.clear()\n        log.info(\"finished stopping the stream manager\")\n\n    def reflect_stream(self, stream: ManagedStream, server: Optional[str] = None,\n                       port: Optional[int] = None) -> asyncio.Task:\n        if not server or not port:\n            server, port = random.choice(self.config.reflector_servers)\n        if stream.sd_hash in self.running_reflector_uploads:\n            return self.running_reflector_uploads[stream.sd_hash]\n        task = self.loop.create_task(self._retriable_reflect_stream(stream, server, port))\n        self.running_reflector_uploads[stream.sd_hash] = task\n        task.add_done_callback(\n            lambda _: None if stream.sd_hash not in self.running_reflector_uploads else\n            self.running_reflector_uploads.pop(stream.sd_hash)\n        )\n        return task\n\n    @staticmethod\n    async def _retriable_reflect_stream(stream, host, port):\n        sent = await stream.upload_to_reflector(host, port)\n        while not stream.is_fully_reflected and stream.reflector_progress > 0 and len(sent) > 0:\n            stream.reflector_progress = 0\n            sent = await stream.upload_to_reflector(host, port)\n        return sent\n\n    async def create(self, file_path: str, key: Optional[bytes] = None,\n                     iv_generator: Optional[typing.Generator[bytes, None, None]] = None) -> ManagedStream:\n        descriptor = await StreamDescriptor.create_stream(\n            self.loop, self.blob_manager.blob_dir, file_path, key=key, iv_generator=iv_generator,\n            blob_completed_callback=self.blob_manager.blob_completed\n        )\n        await self.storage.store_stream(\n            self.blob_manager.get_blob(descriptor.sd_hash, is_mine=True), descriptor\n        )\n        row_id = await self.storage.save_published_file(\n            descriptor.stream_hash, os.path.basename(file_path), os.path.dirname(file_path), 0\n        )\n        stream = ManagedStream(\n            self.loop, self.config, self.blob_manager, descriptor.sd_hash, os.path.dirname(file_path),\n            os.path.basename(file_path), status=ManagedDownloadSource.STATUS_FINISHED,\n            rowid=row_id, descriptor=descriptor\n        )\n        self.streams[stream.sd_hash] = stream\n        self.storage.content_claim_callbacks[stream.stream_hash] = lambda: self._update_content_claim(stream)\n        if self.config.reflect_streams and self.config.reflector_servers:\n            self.reflect_stream(stream)\n        return stream\n\n    async def delete(self, source: ManagedDownloadSource, delete_file: Optional[bool] = False):\n        if not isinstance(source, ManagedStream):\n            return\n        if source.identifier in self.running_reflector_uploads:\n            self.running_reflector_uploads[source.identifier].cancel()\n        await source.stop_tasks()\n        if source.identifier in self.streams:\n            del self.streams[source.identifier]\n        blob_hashes = [source.identifier] + [b.blob_hash for b in source.descriptor.blobs[:-1]]\n        await self.blob_manager.delete_blobs(blob_hashes, delete_from_db=False)\n        await self.storage.delete_stream(source.descriptor)\n        if delete_file and source.output_file_exists:\n            os.remove(source.full_path)\n\n    async def stream_partial_content(self, request: Request, sd_hash: str):\n        stream = self._sources[sd_hash]\n        if not stream.downloader.node:\n            stream.downloader.node = self.node\n        return await stream.stream_file(request)\n"
  },
  {
    "path": "lbry/testcase.py",
    "content": "import os\nimport sys\nimport json\nimport shutil\nimport logging\nimport tempfile\nimport functools\nimport asyncio\nfrom asyncio.runners import _cancel_all_tasks  # type: ignore\nimport unittest\nfrom unittest.case import _Outcome\nfrom typing import Optional\nfrom time import time\nfrom binascii import unhexlify\nfrom functools import partial\n\nfrom lbry.wallet import WalletManager, Wallet, Ledger, Account, Transaction\nfrom lbry.conf import Config\nfrom lbry.wallet.util import satoshis_to_coins\nfrom lbry.wallet.dewies import lbc_to_dewies\nfrom lbry.wallet.orchstr8 import Conductor\nfrom lbry.wallet.orchstr8.node import LBCWalletNode, WalletNode\nfrom lbry.schema.claim import Claim\n\nfrom lbry.extras.daemon.daemon import Daemon, jsonrpc_dumps_pretty\nfrom lbry.extras.daemon.components import Component, WalletComponent\nfrom lbry.extras.daemon.components import (\n    DHT_COMPONENT,\n    HASH_ANNOUNCER_COMPONENT, PEER_PROTOCOL_SERVER_COMPONENT,\n    UPNP_COMPONENT, EXCHANGE_RATE_MANAGER_COMPONENT, LIBTORRENT_COMPONENT\n)\nfrom lbry.extras.daemon.componentmanager import ComponentManager\nfrom lbry.extras.daemon.exchange_rate_manager import (\n    ExchangeRateManager, ExchangeRate, BittrexBTCFeed, BittrexUSDFeed\n)\nfrom lbry.extras.daemon.storage import SQLiteStorage\nfrom lbry.blob.blob_manager import BlobManager\nfrom lbry.stream.reflector.server import ReflectorServer\nfrom lbry.blob_exchange.server import BlobServer\n\n\nclass ColorHandler(logging.StreamHandler):\n\n    level_color = {\n        logging.DEBUG: \"black\",\n        logging.INFO: \"light_gray\",\n        logging.WARNING: \"yellow\",\n        logging.ERROR: \"red\"\n    }\n\n    color_code = dict(\n        black=30,\n        red=31,\n        green=32,\n        yellow=33,\n        blue=34,\n        magenta=35,\n        cyan=36,\n        white=37,\n        light_gray='0;37',\n        dark_gray='1;30'\n    )\n\n    def emit(self, record):\n        try:\n            msg = self.format(record)\n            color_name = self.level_color.get(record.levelno, \"black\")\n            color_code = self.color_code[color_name]\n            stream = self.stream\n            stream.write(f'\\x1b[{color_code}m{msg}\\x1b[0m')\n            stream.write(self.terminator)\n            self.flush()\n        except Exception:\n            self.handleError(record)\n\n\nHANDLER = ColorHandler(sys.stdout)\nHANDLER.setFormatter(\n    logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')\n)\nlogging.getLogger().addHandler(HANDLER)\n\n\nclass AsyncioTestCase(unittest.TestCase):\n    # Implementation inspired by discussion:\n    #  https://bugs.python.org/issue32972\n\n    LOOP_SLOW_CALLBACK_DURATION = 0.2\n    TIMEOUT = 120.0\n\n    maxDiff = None\n\n    async def asyncSetUp(self):  # pylint: disable=C0103\n        pass\n\n    async def asyncTearDown(self):  # pylint: disable=C0103\n        pass\n\n    def run(self, result=None):  # pylint: disable=R0915\n        orig_result = result\n        if result is None:\n            result = self.defaultTestResult()\n            startTestRun = getattr(result, 'startTestRun', None)  # pylint: disable=C0103\n            if startTestRun is not None:\n                startTestRun()\n\n        result.startTest(self)\n\n        testMethod = getattr(self, self._testMethodName)  # pylint: disable=C0103\n        if (getattr(self.__class__, \"__unittest_skip__\", False) or\n                getattr(testMethod, \"__unittest_skip__\", False)):\n            # If the class or method was skipped.\n            try:\n                skip_why = (getattr(self.__class__, '__unittest_skip_why__', '')\n                            or getattr(testMethod, '__unittest_skip_why__', ''))\n                self._addSkip(result, self, skip_why)\n            finally:\n                result.stopTest(self)\n            return\n        expecting_failure_method = getattr(testMethod,\n                                           \"__unittest_expecting_failure__\", False)\n        expecting_failure_class = getattr(self,\n                                          \"__unittest_expecting_failure__\", False)\n        expecting_failure = expecting_failure_class or expecting_failure_method\n        outcome = _Outcome(result)\n\n        self.loop = asyncio.new_event_loop()  # pylint: disable=W0201\n        asyncio.set_event_loop(self.loop)\n        self.loop.set_debug(True)\n        self.loop.slow_callback_duration = self.LOOP_SLOW_CALLBACK_DURATION\n\n        try:\n            self._outcome = outcome\n\n            with outcome.testPartExecutor(self):\n                self.setUp()\n                self.add_timeout()\n                self.loop.run_until_complete(self.asyncSetUp())\n            if outcome.success:\n                outcome.expecting_failure = expecting_failure\n                with outcome.testPartExecutor(self, isTest=True):\n                    maybe_coroutine = testMethod()\n                    if asyncio.iscoroutine(maybe_coroutine):\n                        self.add_timeout()\n                        self.loop.run_until_complete(maybe_coroutine)\n                outcome.expecting_failure = False\n                with outcome.testPartExecutor(self):\n                    self.add_timeout()\n                    self.loop.run_until_complete(self.asyncTearDown())\n                    self.tearDown()\n\n            self.doAsyncCleanups()\n\n            try:\n                _cancel_all_tasks(self.loop)\n                self.loop.run_until_complete(self.loop.shutdown_asyncgens())\n            finally:\n                asyncio.set_event_loop(None)\n                self.loop.close()\n\n            for test, reason in outcome.skipped:\n                self._addSkip(result, test, reason)\n            self._feedErrorsToResult(result, outcome.errors)\n            if outcome.success:\n                if expecting_failure:\n                    if outcome.expectedFailure:\n                        self._addExpectedFailure(result, outcome.expectedFailure)\n                    else:\n                        self._addUnexpectedSuccess(result)\n                else:\n                    result.addSuccess(self)\n            return result\n        finally:\n            result.stopTest(self)\n            if orig_result is None:\n                stopTestRun = getattr(result, 'stopTestRun', None)  # pylint: disable=C0103\n                if stopTestRun is not None:\n                    stopTestRun()  # pylint: disable=E1102\n\n            # explicitly break reference cycles:\n            # outcome.errors -> frame -> outcome -> outcome.errors\n            # outcome.expectedFailure -> frame -> outcome -> outcome.expectedFailure\n            outcome.errors.clear()\n            outcome.expectedFailure = None\n\n            # clear the outcome, no more needed\n            self._outcome = None\n\n    def doAsyncCleanups(self):  # pylint: disable=C0103\n        outcome = self._outcome or _Outcome()\n        while self._cleanups:\n            function, args, kwargs = self._cleanups.pop()\n            with outcome.testPartExecutor(self):\n                maybe_coroutine = function(*args, **kwargs)\n                if asyncio.iscoroutine(maybe_coroutine):\n                    self.add_timeout()\n                    self.loop.run_until_complete(maybe_coroutine)\n\n    def cancel(self):\n        for task in asyncio.all_tasks(self.loop):\n            if not task.done():\n                task.print_stack()\n                task.cancel()\n\n    def add_timeout(self):\n        if self.TIMEOUT:\n            self.loop.call_later(self.TIMEOUT, self.check_timeout, time())\n\n    def check_timeout(self, started):\n        if time() - started >= self.TIMEOUT:\n            self.cancel()\n        else:\n            self.loop.call_later(self.TIMEOUT, self.check_timeout, started)\n\n\nclass AdvanceTimeTestCase(AsyncioTestCase):\n\n    async def asyncSetUp(self):\n        self._time = 0  # pylint: disable=W0201\n        self.loop.time = functools.wraps(self.loop.time)(lambda: self._time)\n        await super().asyncSetUp()\n\n    async def advance(self, seconds):\n        while self.loop._ready:\n            await asyncio.sleep(0)\n        self._time += seconds\n        await asyncio.sleep(0)\n        while self.loop._ready:\n            await asyncio.sleep(0)\n\n\nclass IntegrationTestCase(AsyncioTestCase):\n\n    SEED = None\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.conductor: Optional[Conductor] = None\n        self.blockchain: Optional[LBCWalletNode] = None\n        self.wallet_node: Optional[WalletNode] = None\n        self.manager: Optional[WalletManager] = None\n        self.ledger: Optional[Ledger] = None\n        self.wallet: Optional[Wallet] = None\n        self.account: Optional[Account] = None\n\n    async def asyncSetUp(self):\n        self.conductor = Conductor(seed=self.SEED)\n        await self.conductor.start_lbcd()\n        self.addCleanup(self.conductor.stop_lbcd)\n        await self.conductor.start_lbcwallet()\n        self.addCleanup(self.conductor.stop_lbcwallet)\n        await self.conductor.start_spv()\n        self.addCleanup(self.conductor.stop_spv)\n        await self.conductor.start_wallet()\n        self.addCleanup(self.conductor.stop_wallet)\n        self.blockchain = self.conductor.lbcwallet_node\n        self.wallet_node = self.conductor.wallet_node\n        self.manager = self.wallet_node.manager\n        self.ledger = self.wallet_node.ledger\n        self.wallet = self.wallet_node.wallet\n        self.account = self.wallet_node.wallet.default_account\n\n    async def assertBalance(self, account, expected_balance: str):  # pylint: disable=C0103\n        balance = await account.get_balance()\n        self.assertEqual(satoshis_to_coins(balance), expected_balance)\n\n    def broadcast(self, tx):\n        return self.ledger.broadcast(tx)\n\n    async def broadcast_and_confirm(self, tx, ledger=None):\n        ledger = ledger or self.ledger\n        notifications = asyncio.create_task(ledger.wait(tx))\n        await ledger.broadcast(tx)\n        await notifications\n        await self.generate_and_wait(1, [tx.id], ledger)\n\n    async def on_header(self, height):\n        if self.ledger.headers.height < height:\n            await self.ledger.on_header.where(\n                lambda e: e.height == height\n            )\n        return True\n\n    async def send_to_address_and_wait(self, address, amount, blocks_to_generate=0, ledger=None):\n        tx_watch = []\n        txid = None\n        done = False\n        watcher = (ledger or self.ledger).on_transaction.where(\n            lambda e: e.tx.id == txid or done or tx_watch.append(e.tx.id)\n        )\n\n        txid = await self.blockchain.send_to_address(address, amount)\n        done = txid in tx_watch\n        await watcher\n\n        await self.generate_and_wait(blocks_to_generate, [txid], ledger)\n        return txid\n\n    async def generate_and_wait(self, blocks_to_generate, txids, ledger=None):\n        if blocks_to_generate > 0:\n            watcher = (ledger or self.ledger).on_transaction.where(\n                lambda e: ((e.tx.id in txids and txids.remove(e.tx.id)), len(txids) <= 0)[-1]  # multi-statement lambda\n            )\n            await self.generate(blocks_to_generate)\n            await watcher\n\n    def on_address_update(self, address):\n        return self.ledger.on_transaction.where(\n            lambda e: e.address == address\n        )\n\n    def on_transaction_address(self, tx, address):\n        return self.ledger.on_transaction.where(\n            lambda e: e.tx.id == tx.id and e.address == address\n        )\n\n    async def generate(self, blocks):\n        \"\"\" Ask lbrycrd to generate some blocks and wait until ledger has them. \"\"\"\n        prepare = self.ledger.on_header.where(self.blockchain.is_expected_block)\n        self.conductor.spv_node.server.synchronized.clear()\n        await self.blockchain.generate(blocks)\n        height = self.blockchain.block_expected\n        await prepare  # no guarantee that it didn't happen already, so start waiting from before calling generate\n        while True:\n            await self.conductor.spv_node.server.synchronized.wait()\n            self.conductor.spv_node.server.synchronized.clear()\n            if self.conductor.spv_node.server.db.db_height < height:\n                continue\n            if self.conductor.spv_node.server._es_height < height:\n                continue\n            break\n\n\nclass FakeExchangeRateManager(ExchangeRateManager):\n\n    def __init__(self, market_feeds, rates):  # pylint: disable=super-init-not-called\n        self.market_feeds = market_feeds\n        for feed in self.market_feeds:\n            feed.last_check = time()\n            feed.rate = ExchangeRate(feed.market, rates[feed.market], time())\n\n    def start(self):\n        pass\n\n    def stop(self):\n        pass\n\n\ndef get_fake_exchange_rate_manager(rates=None):\n    return FakeExchangeRateManager(\n        [BittrexBTCFeed(), BittrexUSDFeed()],\n        rates or {'BTCLBC': 3.0, 'USDLBC': 2.0}\n    )\n\n\nclass ExchangeRateManagerComponent(Component):\n    component_name = EXCHANGE_RATE_MANAGER_COMPONENT\n\n    def __init__(self, component_manager, rates=None):\n        super().__init__(component_manager)\n        self.exchange_rate_manager = get_fake_exchange_rate_manager(rates)\n\n    @property\n    def component(self) -> ExchangeRateManager:\n        return self.exchange_rate_manager\n\n    async def start(self):\n        self.exchange_rate_manager.start()\n\n    async def stop(self):\n        self.exchange_rate_manager.stop()\n\n\nclass CommandTestCase(IntegrationTestCase):\n\n    VERBOSITY = logging.WARN\n    blob_lru_cache_size = 0\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.daemon = None\n        self.daemons = []\n        self.server_config = None\n        self.server_storage = None\n        self.extra_wallet_nodes = []\n        self.extra_wallet_node_port = 5280\n        self.server_blob_manager = None\n        self.server = None\n        self.reflector = None\n        self.skip_libtorrent = True\n\n    async def asyncSetUp(self):\n\n        logging.getLogger('lbry.blob_exchange').setLevel(self.VERBOSITY)\n        logging.getLogger('lbry.daemon').setLevel(self.VERBOSITY)\n        logging.getLogger('lbry.stream').setLevel(self.VERBOSITY)\n        logging.getLogger('lbry.wallet').setLevel(self.VERBOSITY)\n\n        await super().asyncSetUp()\n\n        self.daemon = await self.add_daemon(self.wallet_node)\n\n        await self.account.ensure_address_gap()\n        address = (await self.account.receiving.get_addresses(limit=1, only_usable=True))[0]\n        await self.send_to_address_and_wait(address, 10, 6)\n\n        server_tmp_dir = tempfile.mkdtemp()\n        self.addCleanup(shutil.rmtree, server_tmp_dir)\n        self.server_config = Config(\n            data_dir=server_tmp_dir,\n            wallet_dir=server_tmp_dir,\n            save_files=True,\n            download_dir=server_tmp_dir\n        )\n        self.server_config.transaction_cache_size = 10000\n        self.server_storage = SQLiteStorage(self.server_config, ':memory:')\n        await self.server_storage.open()\n\n        self.server_blob_manager = BlobManager(self.loop, server_tmp_dir, self.server_storage, self.server_config)\n        self.server = BlobServer(self.loop, self.server_blob_manager, 'bQEaw42GXsgCAGio1nxFncJSyRmnztSCjP')\n        self.server.start_server(5567, '127.0.0.1')\n        await self.server.started_listening.wait()\n\n        self.reflector = ReflectorServer(self.server_blob_manager)\n        self.reflector.start_server(5566, '127.0.0.1')\n        await self.reflector.started_listening.wait()\n        self.addCleanup(self.reflector.stop_server)\n\n    async def asyncTearDown(self):\n        await super().asyncTearDown()\n        for wallet_node in self.extra_wallet_nodes:\n            await wallet_node.stop(cleanup=True)\n        for daemon in self.daemons:\n            daemon.component_manager.get_component('wallet')._running = False\n            await daemon.stop()\n\n    async def add_daemon(self, wallet_node=None, seed=None):\n        start_wallet_node = False\n        if wallet_node is None:\n            wallet_node = WalletNode(\n                self.wallet_node.manager_class,\n                self.wallet_node.ledger_class,\n                port=self.extra_wallet_node_port\n            )\n            self.extra_wallet_node_port += 1\n            start_wallet_node = True\n\n        upload_dir = os.path.join(wallet_node.data_path, 'uploads')\n        os.mkdir(upload_dir)\n\n        conf = Config(\n            # needed during instantiation to access known_hubs path\n            data_dir=wallet_node.data_path,\n            wallet_dir=wallet_node.data_path,\n            save_files=True,\n            download_dir=wallet_node.data_path\n        )\n        conf.upload_dir = upload_dir  # not a real conf setting\n        conf.share_usage_data = False\n        conf.use_upnp = False\n        conf.reflect_streams = True\n        conf.blockchain_name = 'lbrycrd_regtest'\n        conf.lbryum_servers = [(self.conductor.spv_node.hostname, self.conductor.spv_node.port)]\n        conf.reflector_servers = [('127.0.0.1', 5566)]\n        conf.fixed_peers = [('127.0.0.1', 5567)]\n        conf.known_dht_nodes = []\n        conf.blob_lru_cache_size = self.blob_lru_cache_size\n        conf.transaction_cache_size = 10000\n        conf.components_to_skip = [\n            DHT_COMPONENT, UPNP_COMPONENT, HASH_ANNOUNCER_COMPONENT,\n            PEER_PROTOCOL_SERVER_COMPONENT\n        ]\n        if self.skip_libtorrent:\n            conf.components_to_skip.append(LIBTORRENT_COMPONENT)\n\n        if start_wallet_node:\n            await wallet_node.start(self.conductor.spv_node, seed=seed, config=conf)\n            self.extra_wallet_nodes.append(wallet_node)\n        else:\n            wallet_node.manager.config = conf\n            wallet_node.manager.ledger.config['known_hubs'] = conf.known_hubs\n\n        def wallet_maker(component_manager):\n            wallet_component = WalletComponent(component_manager)\n            wallet_component.wallet_manager = wallet_node.manager\n            wallet_component._running = True\n            return wallet_component\n\n        daemon = Daemon(conf, ComponentManager(\n            conf, skip_components=conf.components_to_skip, wallet=wallet_maker,\n            exchange_rate_manager=partial(ExchangeRateManagerComponent, rates={\n                'BTCLBC': 1.0, 'USDLBC': 2.0\n            })\n        ))\n        await daemon.initialize()\n        self.daemons.append(daemon)\n        wallet_node.manager.old_db = daemon.storage\n        return daemon\n\n    async def confirm_tx(self, txid, ledger=None):\n        \"\"\" Wait for tx to be in mempool, then generate a block, wait for tx to be in a block. \"\"\"\n        # await (ledger or self.ledger).on_transaction.where(lambda e: e.tx.id == txid)\n        on_tx = (ledger or self.ledger).on_transaction.where(lambda e: e.tx.id == txid)\n        await asyncio.wait([self.generate(1), on_tx], timeout=5)\n\n        # # actually, if it's in the mempool or in the block we're fine\n        # await self.generate_and_wait(1, [txid], ledger=ledger)\n        # return txid\n\n        return txid\n\n    async def on_transaction_dict(self, tx):\n        await self.ledger.wait(Transaction(unhexlify(tx['hex'])))\n\n    @staticmethod\n    def get_all_addresses(tx):\n        addresses = set()\n        for txi in tx['inputs']:\n            addresses.add(txi['address'])\n        for txo in tx['outputs']:\n            addresses.add(txo['address'])\n        return list(addresses)\n\n    async def blockchain_claim_name(self, name: str, value: str, amount: str, confirm=True):\n        txid = await self.blockchain._cli_cmnd('claimname', name, value, amount)\n        if confirm:\n            await self.generate(1)\n        return txid\n\n    async def blockchain_update_name(self, txid: str, value: str, amount: str, confirm=True):\n        txid = await self.blockchain._cli_cmnd('updateclaim', txid, value, amount)\n        if confirm:\n            await self.generate(1)\n        return txid\n\n    async def out(self, awaitable):\n        \"\"\" Serializes lbrynet API results to JSON then loads and returns it as dictionary. \"\"\"\n        return json.loads(jsonrpc_dumps_pretty(await awaitable, ledger=self.ledger))['result']\n\n    def sout(self, value):\n        \"\"\" Synchronous version of `out` method. \"\"\"\n        return json.loads(jsonrpc_dumps_pretty(value, ledger=self.ledger))['result']\n\n    async def confirm_and_render(self, awaitable, confirm, return_tx=False) -> Transaction:\n        tx = await awaitable\n        if confirm:\n            await self.ledger.wait(tx)\n            await self.generate(1)\n            await self.ledger.wait(tx, self.blockchain.block_expected)\n        if not return_tx:\n            return self.sout(tx)\n        return tx\n\n    async def create_nondeterministic_channel(self, name, price, pubkey_bytes, daemon=None, blocking=False):\n        account = (daemon or self.daemon).wallet_manager.default_account\n        claim_address = await account.receiving.get_or_create_usable_address()\n        claim = Claim()\n        claim.channel.public_key_bytes = pubkey_bytes\n        tx = await Transaction.claim_create(\n            name, claim, lbc_to_dewies(price),\n            claim_address, [self.account], self.account\n        )\n        await tx.sign([self.account])\n        await (daemon or self.daemon).broadcast_or_release(tx, blocking)\n        return self.sout(tx)\n\n    def create_upload_file(self, data, prefix=None, suffix=None):\n        file_path = tempfile.mktemp(prefix=prefix or \"tmp\", suffix=suffix or \"\", dir=self.daemon.conf.upload_dir)\n        with open(file_path, 'w+b') as file:\n            file.write(data)\n            file.flush()\n            return file.name\n\n    async def stream_create(\n            self, name='hovercraft', bid='1.0', file_path=None,\n            data=b'hi!', confirm=True, prefix=None, suffix=None, return_tx=False, **kwargs):\n        if file_path is None and data is not None:\n            file_path = self.create_upload_file(data=data, prefix=prefix, suffix=suffix)\n        return await self.confirm_and_render(\n            self.daemon.jsonrpc_stream_create(name, bid, file_path=file_path, **kwargs), confirm, return_tx\n        )\n\n    async def stream_update(\n            self, claim_id, data=None, prefix=None, suffix=None, confirm=True, return_tx=False, **kwargs):\n        if data is not None:\n            file_path = self.create_upload_file(data=data, prefix=prefix, suffix=suffix)\n            return await self.confirm_and_render(\n                self.daemon.jsonrpc_stream_update(claim_id, file_path=file_path, **kwargs), confirm, return_tx\n            )\n        return await self.confirm_and_render(\n            self.daemon.jsonrpc_stream_update(claim_id, **kwargs), confirm\n        )\n\n    async def stream_repost(self, claim_id, name='repost', bid='1.0', confirm=True, **kwargs):\n        return await self.confirm_and_render(\n            self.daemon.jsonrpc_stream_repost(claim_id=claim_id, name=name, bid=bid, **kwargs), confirm\n        )\n\n    async def stream_abandon(self, *args, confirm=True, **kwargs):\n        if 'blocking' not in kwargs:\n            kwargs['blocking'] = False\n        return await self.confirm_and_render(\n            self.daemon.jsonrpc_stream_abandon(*args, **kwargs), confirm\n        )\n\n    async def purchase_create(self, *args, confirm=True, **kwargs):\n        return await self.confirm_and_render(\n            self.daemon.jsonrpc_purchase_create(*args, **kwargs), confirm\n        )\n\n    async def publish(self, name, *args, confirm=True, **kwargs):\n        return await self.confirm_and_render(\n            self.daemon.jsonrpc_publish(name, *args, **kwargs), confirm\n        )\n\n    async def channel_create(self, name='@arena', bid='1.0', confirm=True, **kwargs):\n        return await self.confirm_and_render(\n            self.daemon.jsonrpc_channel_create(name, bid, **kwargs), confirm\n        )\n\n    async def channel_update(self, claim_id, confirm=True, **kwargs):\n        return await self.confirm_and_render(\n            self.daemon.jsonrpc_channel_update(claim_id, **kwargs), confirm\n        )\n\n    async def channel_abandon(self, *args, confirm=True, **kwargs):\n        if 'blocking' not in kwargs:\n            kwargs['blocking'] = False\n        return await self.confirm_and_render(\n            self.daemon.jsonrpc_channel_abandon(*args, **kwargs), confirm\n        )\n\n    async def collection_create(\n            self, name='firstcollection', bid='1.0', confirm=True, **kwargs):\n        return await self.confirm_and_render(\n            self.daemon.jsonrpc_collection_create(name, bid, **kwargs), confirm\n        )\n\n    async def collection_update(\n            self, claim_id, confirm=True, **kwargs):\n        return await self.confirm_and_render(\n            self.daemon.jsonrpc_collection_update(claim_id, **kwargs), confirm\n        )\n\n    async def collection_abandon(self, *args, confirm=True, **kwargs):\n        if 'blocking' not in kwargs:\n            kwargs['blocking'] = False\n        return await self.confirm_and_render(\n            self.daemon.jsonrpc_stream_abandon(*args, **kwargs), confirm\n        )\n\n    async def support_create(self, claim_id, bid='1.0', confirm=True, **kwargs):\n        return await self.confirm_and_render(\n            self.daemon.jsonrpc_support_create(claim_id, bid, **kwargs), confirm\n        )\n\n    async def support_abandon(self, *args, confirm=True, **kwargs):\n        if 'blocking' not in kwargs:\n            kwargs['blocking'] = False\n        return await self.confirm_and_render(\n            self.daemon.jsonrpc_support_abandon(*args, **kwargs), confirm\n        )\n\n    async def account_send(self, *args, confirm=True, **kwargs):\n        return await self.confirm_and_render(\n            self.daemon.jsonrpc_account_send(*args, **kwargs), confirm\n        )\n\n    async def wallet_send(self, *args, confirm=True, **kwargs):\n        return await self.confirm_and_render(\n            self.daemon.jsonrpc_wallet_send(*args, **kwargs), confirm\n        )\n\n    async def txo_spend(self, *args, confirm=True, **kwargs):\n        txs = await self.daemon.jsonrpc_txo_spend(*args, **kwargs)\n        if confirm:\n            await asyncio.wait([self.ledger.wait(tx) for tx in txs])\n            await self.generate(1)\n            await asyncio.wait([self.ledger.wait(tx, self.blockchain.block_expected) for tx in txs])\n        return self.sout(txs)\n\n    async def blob_clean(self):\n        return await self.out(self.daemon.jsonrpc_blob_clean())\n\n    async def status(self):\n        return await self.out(self.daemon.jsonrpc_status())\n\n    async def resolve(self, uri, **kwargs):\n        return (await self.out(self.daemon.jsonrpc_resolve(uri, **kwargs)))[uri]\n\n    async def claim_search(self, **kwargs):\n        return (await self.out(self.daemon.jsonrpc_claim_search(**kwargs)))['items']\n\n    async def get_claim_by_claim_id(self, claim_id):\n        return await self.out(self.ledger.get_claim_by_claim_id(claim_id))\n\n    async def file_list(self, *args, **kwargs):\n        return (await self.out(self.daemon.jsonrpc_file_list(*args, **kwargs)))['items']\n\n    async def txo_list(self, *args, **kwargs):\n        return (await self.out(self.daemon.jsonrpc_txo_list(*args, **kwargs)))['items']\n\n    async def txo_sum(self, *args, **kwargs):\n        return await self.out(self.daemon.jsonrpc_txo_sum(*args, **kwargs))\n\n    async def txo_plot(self, *args, **kwargs):\n        return await self.out(self.daemon.jsonrpc_txo_plot(*args, **kwargs))\n\n    async def claim_list(self, *args, **kwargs):\n        return (await self.out(self.daemon.jsonrpc_claim_list(*args, **kwargs)))['items']\n\n    async def stream_list(self, *args, **kwargs):\n        return (await self.out(self.daemon.jsonrpc_stream_list(*args, **kwargs)))['items']\n\n    async def channel_list(self, *args, **kwargs):\n        return (await self.out(self.daemon.jsonrpc_channel_list(*args, **kwargs)))['items']\n\n    async def transaction_list(self, *args, **kwargs):\n        return (await self.out(self.daemon.jsonrpc_transaction_list(*args, **kwargs)))['items']\n\n    async def blob_list(self, *args, **kwargs):\n        return (await self.out(self.daemon.jsonrpc_blob_list(*args, **kwargs)))['items']\n\n    @staticmethod\n    def get_claim_id(tx):\n        return tx['outputs'][0]['claim_id']\n\n    def assertItemCount(self, result, count):  # pylint: disable=invalid-name\n        self.assertEqual(count, result['total_items'])\n"
  },
  {
    "path": "lbry/torrent/__init__.py",
    "content": ""
  },
  {
    "path": "lbry/torrent/session.py",
    "content": "import asyncio\nimport binascii\nimport os\nimport logging\nimport random\nfrom hashlib import sha1\nfrom tempfile import mkdtemp\nfrom typing import Optional\n\nimport libtorrent\n\n\nlog = logging.getLogger(__name__)\nDEFAULT_FLAGS = (  # fixme: somehow the logic here is inverted?\n        libtorrent.add_torrent_params_flags_t.flag_auto_managed\n        | libtorrent.add_torrent_params_flags_t.flag_update_subscribe\n)\n\n\nclass TorrentHandle:\n    def __init__(self, loop, executor, handle):\n        self._loop = loop\n        self._executor = executor\n        self._handle: libtorrent.torrent_handle = handle\n        self.started = asyncio.Event(loop=loop)\n        self.finished = asyncio.Event(loop=loop)\n        self.metadata_completed = asyncio.Event(loop=loop)\n        self.size = 0\n        self.total_wanted_done = 0\n        self.name = ''\n        self.tasks = []\n        self.torrent_file: Optional[libtorrent.file_storage] = None\n        self._base_path = None\n        self._handle.set_sequential_download(1)\n\n    @property\n    def largest_file(self) -> Optional[str]:\n        if not self.torrent_file:\n            return None\n        index = self.largest_file_index\n        return os.path.join(self._base_path, self.torrent_file.at(index).path)\n\n    @property\n    def largest_file_index(self):\n        largest_size, index = 0, 0\n        for file_num in range(self.torrent_file.num_files()):\n            if self.torrent_file.file_size(file_num) > largest_size:\n                largest_size = self.torrent_file.file_size(file_num)\n                index = file_num\n        return index\n\n    def stop_tasks(self):\n        while self.tasks:\n            self.tasks.pop().cancel()\n\n    def _show_status(self):\n        # fixme: cleanup\n        if not self._handle.is_valid():\n            return\n        status = self._handle.status()\n        if status.has_metadata:\n            self.size = status.total_wanted\n            self.total_wanted_done = status.total_wanted_done\n            self.name = status.name\n            if not self.metadata_completed.is_set():\n                self.metadata_completed.set()\n                log.info(\"Metadata completed for btih:%s - %s\", status.info_hash, self.name)\n                self.torrent_file = self._handle.get_torrent_info().files()\n                self._base_path = status.save_path\n            first_piece = self.torrent_file.at(self.largest_file_index).offset\n            if not self.started.is_set():\n                if self._handle.have_piece(first_piece):\n                    self.started.set()\n                else:\n                    # prioritize it\n                    self._handle.set_piece_deadline(first_piece, 100)\n        if not status.is_seeding:\n            log.debug('%.2f%% complete (down: %.1f kB/s up: %.1f kB/s peers: %d seeds: %d) %s - %s',\n                      status.progress * 100, status.download_rate / 1000, status.upload_rate / 1000,\n                      status.num_peers, status.num_seeds, status.state, status.save_path)\n        elif not self.finished.is_set():\n            self.finished.set()\n            log.info(\"Torrent finished: %s\", self.name)\n\n    async def status_loop(self):\n        while True:\n            self._show_status()\n            if self.finished.is_set():\n                break\n            await asyncio.sleep(0.1)\n\n    async def pause(self):\n        await self._loop.run_in_executor(\n            self._executor, self._handle.pause\n        )\n\n    async def resume(self):\n        await self._loop.run_in_executor(\n            self._executor, lambda: self._handle.resume()  # pylint: disable=unnecessary-lambda\n        )\n\n\nclass TorrentSession:\n    def __init__(self, loop, executor):\n        self._loop = loop\n        self._executor = executor\n        self._session: Optional[libtorrent.session] = None\n        self._handles = {}\n        self.tasks = []\n        self.wait_start = True\n\n    async def add_fake_torrent(self):\n        tmpdir = mkdtemp()\n        info, btih = _create_fake_torrent(tmpdir)\n        flags = libtorrent.add_torrent_params_flags_t.flag_seed_mode\n        handle = self._session.add_torrent({\n            'ti': info, 'save_path': tmpdir, 'flags': flags\n        })\n        self._handles[btih] = TorrentHandle(self._loop, self._executor, handle)\n        return btih\n\n    async def bind(self, interface: str = '0.0.0.0', port: int = 10889):\n        settings = {\n            'listen_interfaces': f\"{interface}:{port}\",\n            'enable_natpmp': False,\n            'enable_upnp': False\n        }\n        self._session = await self._loop.run_in_executor(\n            self._executor, libtorrent.session, settings  # pylint: disable=c-extension-no-member\n        )\n        self.tasks.append(self._loop.create_task(self.process_alerts()))\n\n    def stop(self):\n        while self.tasks:\n            self.tasks.pop().cancel()\n        self._session.save_state()\n        self._session.pause()\n        self._session.stop_dht()\n        self._session.stop_lsd()\n        self._session.stop_natpmp()\n        self._session.stop_upnp()\n        self._session = None\n\n    def _pop_alerts(self):\n        for alert in self._session.pop_alerts():\n            log.info(\"torrent alert: %s\", alert)\n\n    async def process_alerts(self):\n        while True:\n            await self._loop.run_in_executor(\n                self._executor, self._pop_alerts\n            )\n            await asyncio.sleep(1)\n\n    async def pause(self):\n        await self._loop.run_in_executor(\n            self._executor, lambda: self._session.save_state()  # pylint: disable=unnecessary-lambda\n        )\n        await self._loop.run_in_executor(\n            self._executor, lambda: self._session.pause()  # pylint: disable=unnecessary-lambda\n        )\n\n    async def resume(self):\n        await self._loop.run_in_executor(\n            self._executor, self._session.resume\n        )\n\n    def _add_torrent(self, btih: str, download_directory: Optional[str]):\n        params = {'info_hash': binascii.unhexlify(btih.encode()), 'flags': DEFAULT_FLAGS}\n        if download_directory:\n            params['save_path'] = download_directory\n        handle = self._session.add_torrent(params)\n        handle.force_dht_announce()\n        self._handles[btih] = TorrentHandle(self._loop, self._executor, handle)\n\n    def full_path(self, btih):\n        return self._handles[btih].largest_file\n\n    async def add_torrent(self, btih, download_path):\n        await self._loop.run_in_executor(\n            self._executor, self._add_torrent, btih, download_path\n        )\n        self._handles[btih].tasks.append(self._loop.create_task(self._handles[btih].status_loop()))\n        await self._handles[btih].metadata_completed.wait()\n        if self.wait_start:\n            # fixme: temporary until we add streaming support, otherwise playback fails!\n            await self._handles[btih].started.wait()\n\n    def remove_torrent(self, btih, remove_files=False):\n        if btih in self._handles:\n            handle = self._handles[btih]\n            handle.stop_tasks()\n            self._session.remove_torrent(handle._handle, 1 if remove_files else 0)\n            self._handles.pop(btih)\n\n    async def save_file(self, btih, download_directory):\n        handle = self._handles[btih]\n        await handle.resume()\n\n    def get_size(self, btih):\n        return self._handles[btih].size\n\n    def get_name(self, btih):\n        return self._handles[btih].name\n\n    def get_downloaded(self, btih):\n        return self._handles[btih].total_wanted_done\n\n    def is_completed(self, btih):\n        return self._handles[btih].finished.is_set()\n\n\ndef get_magnet_uri(btih):\n    return f\"magnet:?xt=urn:btih:{btih}\"\n\n\ndef _create_fake_torrent(tmpdir):\n    # beware, that's just for testing\n    path = os.path.join(tmpdir, 'tmp')\n    with open(path, 'wb') as myfile:\n        size = myfile.write(bytes([random.randint(0, 255) for _ in range(40)]) * 1024)\n    file_storage = libtorrent.file_storage()\n    file_storage.add_file('tmp', size)\n    t = libtorrent.create_torrent(file_storage, 0, 4 * 1024 * 1024)\n    libtorrent.set_piece_hashes(t, tmpdir)\n    info = libtorrent.torrent_info(t.generate())\n    btih = sha1(info.metadata()).hexdigest()\n    return info, btih\n\n\nasync def main():\n    if os.path.exists(\"~/Downloads/ubuntu-18.04.3-live-server-amd64.torrent\"):\n        os.remove(\"~/Downloads/ubuntu-18.04.3-live-server-amd64.torrent\")\n    if os.path.exists(\"~/Downloads/ubuntu-18.04.3-live-server-amd64.iso\"):\n        os.remove(\"~/Downloads/ubuntu-18.04.3-live-server-amd64.iso\")\n\n    btih = \"dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c\"\n\n    executor = None\n    session = TorrentSession(asyncio.get_event_loop(), executor)\n    session2 = TorrentSession(asyncio.get_event_loop(), executor)\n    await session.bind('localhost', port=4040)\n    await session2.bind('localhost', port=4041)\n    btih = await session.add_fake_torrent()\n    session2._session.add_dht_node(('localhost', 4040))\n    await session2.add_torrent(btih, \"/tmp/down\")\n    while True:\n        await asyncio.sleep(100)\n    await session.pause()\n    executor.shutdown()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "lbry/torrent/torrent.py",
    "content": "import asyncio\nimport logging\nimport typing\n\n\nlog = logging.getLogger(__name__)\n\n\nclass TorrentInfo:\n    __slots__ = ('dht_seeds', 'http_seeds', 'trackers', 'total_size')\n\n    def __init__(self, dht_seeds: typing.Tuple[typing.Tuple[str, int]],\n                 http_seeds: typing.Tuple[typing.Dict[str, typing.Any]],\n                 trackers: typing.Tuple[typing.Tuple[str, int]], total_size: int):\n        self.dht_seeds = dht_seeds\n        self.http_seeds = http_seeds\n        self.trackers = trackers\n        self.total_size = total_size\n\n    @classmethod\n    def from_libtorrent_info(cls, torrent_info):\n        return cls(\n            torrent_info.nodes(), tuple(\n                {\n                    'url': web_seed['url'],\n                    'type': web_seed['type'],\n                    'auth': web_seed['auth']\n                } for web_seed in torrent_info.web_seeds()\n            ), tuple(\n                (tracker.url, tracker.tier) for tracker in torrent_info.trackers()\n            ), torrent_info.total_size()\n        )\n\n\nclass Torrent:\n    def __init__(self, loop, handle):\n        self._loop = loop\n        self._handle = handle\n        self.finished = asyncio.Event()\n\n    def _threaded_update_status(self):\n        status = self._handle.status()\n        if not status.is_seeding:\n            log.info(\n                '%.2f%% complete (down: %.1f kB/s up: %.1f kB/s peers: %d) %s',\n                status.progress * 100, status.download_rate / 1000, status.upload_rate / 1000,\n                status.num_peers, status.state\n            )\n        elif not self.finished.is_set():\n            self.finished.set()\n\n    async def wait_for_finished(self):\n        while True:\n            await self._loop.run_in_executor(\n                None, self._threaded_update_status\n            )\n            if self.finished.is_set():\n                log.info(\"finished downloading torrent!\")\n                await self.pause()\n                break\n            await asyncio.sleep(1)\n\n    async def pause(self):\n        log.info(\"pause torrent\")\n        await self._loop.run_in_executor(\n            None, self._handle.pause\n        )\n\n    async def resume(self):\n        await self._loop.run_in_executor(\n            None, self._handle.resume\n        )\n"
  },
  {
    "path": "lbry/torrent/torrent_manager.py",
    "content": "import asyncio\nimport binascii\nimport logging\nimport os\nimport typing\nfrom typing import Optional\nfrom aiohttp.web import Request\nfrom lbry.file.source_manager import SourceManager\nfrom lbry.file.source import ManagedDownloadSource\n\nif typing.TYPE_CHECKING:\n    from lbry.torrent.session import TorrentSession\n    from lbry.conf import Config\n    from lbry.wallet.transaction import Transaction\n    from lbry.extras.daemon.analytics import AnalyticsManager\n    from lbry.extras.daemon.storage import SQLiteStorage, StoredContentClaim\n    from lbry.extras.daemon.storage import StoredContentClaim\n\nlog = logging.getLogger(__name__)\n\n\ndef path_or_none(encoded_path) -> Optional[str]:\n    if not encoded_path:\n        return\n    return binascii.unhexlify(encoded_path).decode()\n\n\nclass TorrentSource(ManagedDownloadSource):\n    STATUS_STOPPED = \"stopped\"\n    filter_fields = SourceManager.filter_fields\n    filter_fields.update({\n        'bt_infohash'\n    })\n\n    def __init__(self, loop: asyncio.AbstractEventLoop, config: 'Config', storage: 'SQLiteStorage', identifier: str,\n                 file_name: Optional[str] = None, download_directory: Optional[str] = None,\n                 status: Optional[str] = STATUS_STOPPED, claim: Optional['StoredContentClaim'] = None,\n                 download_id: Optional[str] = None, rowid: Optional[int] = None,\n                 content_fee: Optional['Transaction'] = None,\n                 analytics_manager: Optional['AnalyticsManager'] = None,\n                 added_on: Optional[int] = None, torrent_session: Optional['TorrentSession'] = None):\n        super().__init__(loop, config, storage, identifier, file_name, download_directory, status, claim, download_id,\n                         rowid, content_fee, analytics_manager, added_on)\n        self.torrent_session = torrent_session\n\n    @property\n    def full_path(self) -> Optional[str]:\n        full_path = self.torrent_session.full_path(self.identifier)\n        self.download_directory = os.path.dirname(full_path)\n        return full_path\n\n    async def start(self, timeout: Optional[float] = None, save_now: Optional[bool] = False):\n        await self.torrent_session.add_torrent(self.identifier, self.download_directory)\n\n    async def stop(self, finished: bool = False):\n        await self.torrent_session.remove_torrent(self.identifier)\n\n    async def save_file(self, file_name: Optional[str] = None, download_directory: Optional[str] = None):\n        await self.torrent_session.save_file(self.identifier, download_directory)\n\n    @property\n    def torrent_length(self):\n        return self.torrent_session.get_size(self.identifier)\n\n    @property\n    def written_bytes(self):\n        return self.torrent_session.get_downloaded(self.identifier)\n\n    @property\n    def torrent_name(self):\n        return self.torrent_session.get_name(self.identifier)\n\n    @property\n    def bt_infohash(self):\n        return self.identifier\n\n    async def stop_tasks(self):\n        pass\n\n    @property\n    def completed(self):\n        return self.torrent_session.is_completed(self.identifier)\n\n\nclass TorrentManager(SourceManager):\n    _sources: typing.Dict[str, ManagedDownloadSource]\n\n    filter_fields = set(SourceManager.filter_fields)\n    filter_fields.update({\n        'bt_infohash',\n        'blobs_remaining',  # TODO: here they call them \"parts\", but its pretty much the same concept\n        'blobs_in_stream'\n    })\n\n    def __init__(self, loop: asyncio.AbstractEventLoop, config: 'Config', torrent_session: 'TorrentSession',\n                 storage: 'SQLiteStorage', analytics_manager: Optional['AnalyticsManager'] = None):\n        super().__init__(loop, config, storage, analytics_manager)\n        self.torrent_session: 'TorrentSession' = torrent_session\n\n    async def recover_streams(self, file_infos: typing.List[typing.Dict]):\n        raise NotImplementedError\n\n    async def _load_stream(self, rowid: int, bt_infohash: str, file_name: Optional[str],\n                           download_directory: Optional[str], status: str,\n                           claim: Optional['StoredContentClaim'], content_fee: Optional['Transaction'],\n                           added_on: Optional[int]):\n        stream = TorrentSource(\n            self.loop, self.config, self.storage, identifier=bt_infohash, file_name=file_name,\n            download_directory=download_directory, status=status, claim=claim, rowid=rowid,\n            content_fee=content_fee, analytics_manager=self.analytics_manager, added_on=added_on,\n            torrent_session=self.torrent_session\n        )\n        self.add(stream)\n\n    async def initialize_from_database(self):\n        pass\n\n    async def start(self):\n        await super().start()\n\n    async def stop(self):\n        await super().stop()\n        log.info(\"finished stopping the torrent manager\")\n\n    async def delete(self, source: ManagedDownloadSource, delete_file: Optional[bool] = False):\n        await super().delete(source, delete_file)\n        self.torrent_session.remove_torrent(source.identifier, delete_file)\n\n    async def create(self, file_path: str, key: Optional[bytes] = None,\n                     iv_generator: Optional[typing.Generator[bytes, None, None]] = None):\n        raise NotImplementedError\n\n    async def _delete(self, source: ManagedDownloadSource, delete_file: Optional[bool] = False):\n        raise NotImplementedError\n        # blob_hashes = [source.sd_hash] + [b.blob_hash for b in source.descriptor.blobs[:-1]]\n        # await self.blob_manager.delete_blobs(blob_hashes, delete_from_db=False)\n        # await self.storage.delete_stream(source.descriptor)\n\n    async def stream_partial_content(self, request: Request, sd_hash: str):\n        raise NotImplementedError\n"
  },
  {
    "path": "lbry/torrent/tracker.py",
    "content": "import random\nimport socket\nimport string\nimport struct\nimport asyncio\nimport logging\nimport time\nimport ipaddress\nfrom collections import namedtuple\nfrom functools import reduce\nfrom typing import Optional\n\nfrom lbry.dht.node import get_kademlia_peers_from_hosts\nfrom lbry.utils import resolve_host, async_timed_cache, cache_concurrent\nfrom lbry.wallet.stream import StreamController\nfrom lbry import version\n\nlog = logging.getLogger(__name__)\nCONNECTION_EXPIRES_AFTER_SECONDS = 50\nPREFIX = 'LB'  # todo: PR BEP20 to add ourselves\nDEFAULT_TIMEOUT_SECONDS = 10.0\nDEFAULT_CONCURRENCY_LIMIT = 100\n# see: http://bittorrent.org/beps/bep_0015.html and http://xbtt.sourceforge.net/udp_tracker_protocol.html\nConnectRequest = namedtuple(\"ConnectRequest\", [\"connection_id\", \"action\", \"transaction_id\"])\nConnectResponse = namedtuple(\"ConnectResponse\", [\"action\", \"transaction_id\", \"connection_id\"])\nAnnounceRequest = namedtuple(\"AnnounceRequest\",\n                             [\"connection_id\", \"action\", \"transaction_id\", \"info_hash\", \"peer_id\", \"downloaded\", \"left\",\n                              \"uploaded\", \"event\", \"ip_addr\", \"key\", \"num_want\", \"port\"])\nAnnounceResponse = namedtuple(\"AnnounceResponse\",\n                              [\"action\", \"transaction_id\", \"interval\", \"leechers\", \"seeders\", \"peers\"])\nCompactIPv4Peer = namedtuple(\"CompactPeer\", [\"address\", \"port\"])\nScrapeRequest = namedtuple(\"ScrapeRequest\", [\"connection_id\", \"action\", \"transaction_id\", \"infohashes\"])\nScrapeResponse = namedtuple(\"ScrapeResponse\", [\"action\", \"transaction_id\", \"items\"])\nScrapeResponseItem = namedtuple(\"ScrapeResponseItem\", [\"seeders\", \"completed\", \"leechers\"])\nErrorResponse = namedtuple(\"ErrorResponse\", [\"action\", \"transaction_id\", \"message\"])\nstructs = {\n    ConnectRequest: struct.Struct(\">QII\"),\n    ConnectResponse: struct.Struct(\">IIQ\"),\n    AnnounceRequest: struct.Struct(\">QII20s20sQQQIIIiH\"),\n    AnnounceResponse: struct.Struct(\">IIIII\"),\n    CompactIPv4Peer: struct.Struct(\">IH\"),\n    ScrapeRequest: struct.Struct(\">QII\"),\n    ScrapeResponse: struct.Struct(\">II\"),\n    ScrapeResponseItem: struct.Struct(\">III\"),\n    ErrorResponse: struct.Struct(\">II\")\n}\n\n\ndef decode(cls, data, offset=0):\n    decoder = structs[cls]\n    if cls is AnnounceResponse:\n        return AnnounceResponse(*decoder.unpack_from(data, offset),\n                                peers=[decode(CompactIPv4Peer, data, index) for index in range(20, len(data), 6)])\n    elif cls is ScrapeResponse:\n        return ScrapeResponse(*decoder.unpack_from(data, offset),\n                              items=[decode(ScrapeResponseItem, data, index) for index in range(8, len(data), 12)])\n    elif cls is ErrorResponse:\n        return ErrorResponse(*decoder.unpack_from(data, offset), data[decoder.size:])\n    return cls(*decoder.unpack_from(data, offset))\n\n\ndef encode(obj):\n    if isinstance(obj, ScrapeRequest):\n        return structs[ScrapeRequest].pack(*obj[:-1]) + b''.join(obj.infohashes)\n    elif isinstance(obj, ErrorResponse):\n        return structs[ErrorResponse].pack(*obj[:-1]) + obj.message\n    elif isinstance(obj, AnnounceResponse):\n        return structs[AnnounceResponse].pack(*obj[:-1]) + b''.join([encode(peer) for peer in obj.peers])\n    return structs[type(obj)].pack(*obj)\n\n\ndef make_peer_id(random_part: Optional[str] = None) -> bytes:\n    # see https://wiki.theory.org/BitTorrentSpecification#peer_id and https://www.bittorrent.org/beps/bep_0020.html\n    # not to confuse with node id; peer id identifies uniquely the software, version and instance\n    random_part = random_part or ''.join(random.choice(string.ascii_letters) for _ in range(20))\n    return f\"{PREFIX}-{'-'.join(map(str, version))}-{random_part}\"[:20].encode()\n\n\nclass UDPTrackerClientProtocol(asyncio.DatagramProtocol):\n    def __init__(self, timeout: float = DEFAULT_TIMEOUT_SECONDS):\n        self.transport = None\n        self.data_queue = {}\n        self.timeout = timeout\n        self.semaphore = asyncio.Semaphore(DEFAULT_CONCURRENCY_LIMIT)\n\n    def connection_made(self, transport: asyncio.DatagramTransport) -> None:\n        self.transport = transport\n\n    async def request(self, obj, tracker_ip, tracker_port):\n        self.data_queue[obj.transaction_id] = asyncio.get_running_loop().create_future()\n        try:\n            async with self.semaphore:\n                self.transport.sendto(encode(obj), (tracker_ip, tracker_port))\n                return await asyncio.wait_for(self.data_queue[obj.transaction_id], self.timeout)\n        finally:\n            self.data_queue.pop(obj.transaction_id, None)\n\n    async def connect(self, tracker_ip, tracker_port):\n        transaction_id = random.getrandbits(32)\n        return decode(ConnectResponse,\n                      await self.request(ConnectRequest(0x41727101980, 0, transaction_id), tracker_ip, tracker_port))\n\n    @cache_concurrent\n    @async_timed_cache(CONNECTION_EXPIRES_AFTER_SECONDS)\n    async def ensure_connection_id(self, peer_id, tracker_ip, tracker_port):\n        # peer_id is just to ensure cache coherency\n        return (await self.connect(tracker_ip, tracker_port)).connection_id\n\n    async def announce(self, info_hash, peer_id, port, tracker_ip, tracker_port, stopped=False):\n        connection_id = await self.ensure_connection_id(peer_id, tracker_ip, tracker_port)\n        # this should make the key deterministic but unique per info hash + peer id\n        key = int.from_bytes(info_hash[:4], \"big\") ^ int.from_bytes(peer_id[:4], \"big\") ^ port\n        transaction_id = random.getrandbits(32)\n        req = AnnounceRequest(\n            connection_id, 1, transaction_id, info_hash, peer_id, 0, 0, 0, 3 if stopped else 1, 0, key, -1, port)\n        return decode(AnnounceResponse, await self.request(req, tracker_ip, tracker_port))\n\n    async def scrape(self, infohashes, tracker_ip, tracker_port, connection_id=None):\n        connection_id = await self.ensure_connection_id(None, tracker_ip, tracker_port)\n        transaction_id = random.getrandbits(32)\n        reply = await self.request(\n            ScrapeRequest(connection_id, 2, transaction_id, infohashes), tracker_ip, tracker_port)\n        return decode(ScrapeResponse, reply), connection_id\n\n    def datagram_received(self, data: bytes, addr: (str, int)) -> None:\n        if len(data) < 8:\n            return\n        transaction_id = int.from_bytes(data[4:8], byteorder=\"big\", signed=False)\n        if transaction_id in self.data_queue:\n            if not self.data_queue[transaction_id].done():\n                if data[3] == 3:\n                    return self.data_queue[transaction_id].set_exception(Exception(decode(ErrorResponse, data).message))\n                return self.data_queue[transaction_id].set_result(data)\n        log.debug(\"unexpected packet (can be a response for a previously timed out request): %s\", data.hex())\n\n    def connection_lost(self, exc: Exception = None) -> None:\n        self.transport = None\n\n\nclass TrackerClient:\n    event_controller = StreamController()\n\n    def __init__(self, node_id, announce_port, get_servers, timeout=10.0):\n        self.client = UDPTrackerClientProtocol(timeout=timeout)\n        self.transport = None\n        self.peer_id = make_peer_id(node_id.hex() if node_id else None)\n        self.announce_port = announce_port\n        self._get_servers = get_servers\n        self.results = {}  # we can't probe the server before the interval, so we keep the result here until it expires\n        self.tasks = {}\n\n    async def start(self):\n        self.transport, _ = await asyncio.get_running_loop().create_datagram_endpoint(\n            lambda: self.client, local_addr=(\"0.0.0.0\", 0))\n        self.event_controller.stream.listen(\n            lambda request: self.on_hash(request[1], request[2]) if request[0] == 'search' else None)\n\n    def stop(self):\n        while self.tasks:\n            self.tasks.popitem()[1].cancel()\n        if self.transport is not None:\n            self.transport.close()\n        self.client = None\n        self.transport = None\n        self.event_controller.close()\n\n    def on_hash(self, info_hash, on_announcement=None):\n        if info_hash not in self.tasks:\n            task = asyncio.create_task(self.get_peer_list(info_hash, on_announcement=on_announcement))\n            task.add_done_callback(lambda *_: self.tasks.pop(info_hash, None))\n            self.tasks[info_hash] = task\n\n    async def announce_many(self, *info_hashes, stopped=False):\n        await asyncio.gather(\n            *[self._announce_many(server, info_hashes, stopped=stopped) for server in self._get_servers()],\n            return_exceptions=True)\n\n    async def _announce_many(self, server, info_hashes, stopped=False):\n        tracker_ip = await resolve_host(*server, 'udp')\n        still_good_info_hashes = {\n            info_hash for (info_hash, (next_announcement, _)) in self.results.get(tracker_ip, {}).items()\n            if time.time() < next_announcement\n        }\n        results = await asyncio.gather(\n            *[self._probe_server(info_hash, tracker_ip, server[1], stopped=stopped)\n              for info_hash in info_hashes if info_hash not in still_good_info_hashes],\n            return_exceptions=True)\n        if results:\n            errors = sum([1 for result in results if result is None or isinstance(result, Exception)])\n            log.info(\"Tracker: finished announcing %d files to %s:%d, %d errors\", len(results), *server, errors)\n\n    async def get_peer_list(self, info_hash, stopped=False, on_announcement=None, no_port=False):\n        found = []\n        probes = [self._probe_server(info_hash, *server, stopped, no_port) for server in self._get_servers()]\n        for done in asyncio.as_completed(probes):\n            result = await done\n            if result is not None:\n                await asyncio.gather(*filter(asyncio.iscoroutine, [on_announcement(result)] if on_announcement else []))\n                found.append(result)\n        return found\n\n    async def get_kademlia_peer_list(self, info_hash):\n        responses = await self.get_peer_list(info_hash, no_port=True)\n        return await announcement_to_kademlia_peers(*responses)\n\n    async def _probe_server(self, info_hash, tracker_host, tracker_port, stopped=False, no_port=False):\n        result = None\n        try:\n            tracker_host = await resolve_host(tracker_host, tracker_port, 'udp')\n        except socket.error:\n            log.warning(\"DNS failure while resolving tracker host: %s, skipping.\", tracker_host)\n            return\n        self.results.setdefault(tracker_host, {})\n        if info_hash in self.results[tracker_host]:\n            next_announcement, result = self.results[tracker_host][info_hash]\n            if time.time() < next_announcement:\n                return result\n        try:\n            result = await self.client.announce(\n                info_hash, self.peer_id, 0 if no_port else self.announce_port, tracker_host, tracker_port, stopped)\n            self.results[tracker_host][info_hash] = (time.time() + result.interval, result)\n        except asyncio.TimeoutError:  # todo: this is UDP, timeout is common, we need a better metric for failures\n            self.results[tracker_host][info_hash] = (time.time() + 60.0, result)\n            log.debug(\"Tracker timed out: %s:%d\", tracker_host, tracker_port)\n            return None\n        log.debug(\"Announced: %s found %d peers for %s\", tracker_host, len(result.peers), info_hash.hex()[:8])\n        return result\n\n\ndef enqueue_tracker_search(info_hash: bytes, peer_q: asyncio.Queue):\n    async def on_announcement(announcement: AnnounceResponse):\n        peers = await announcement_to_kademlia_peers(announcement)\n        log.info(\"Found %d peers from tracker for %s\", len(peers), info_hash.hex()[:8])\n        peer_q.put_nowait(peers)\n    TrackerClient.event_controller.add(('search', info_hash, on_announcement))\n\n\ndef announcement_to_kademlia_peers(*announcements: AnnounceResponse):\n    peers = [\n        (str(ipaddress.ip_address(peer.address)), peer.port)\n        for announcement in announcements for peer in announcement.peers if peer.port > 1024  # no privileged or 0\n    ]\n    return get_kademlia_peers_from_hosts(peers)\n\n\nclass UDPTrackerServerProtocol(asyncio.DatagramProtocol):  # for testing. Not suitable for production\n    def __init__(self):\n        self.transport = None\n        self.known_conns = set()\n        self.peers = {}\n\n    def connection_made(self, transport: asyncio.DatagramTransport) -> None:\n        self.transport = transport\n\n    def add_peer(self, info_hash, ip_address: str, port: int):\n        self.peers.setdefault(info_hash, [])\n        self.peers[info_hash].append(encode_peer(ip_address, port))\n\n    def datagram_received(self, data: bytes, addr: (str, int)) -> None:\n        if len(data) < 16:\n            return\n        action = int.from_bytes(data[8:12], \"big\", signed=False)\n        if action == 0:\n            req = decode(ConnectRequest, data)\n            connection_id = random.getrandbits(32)\n            self.known_conns.add(connection_id)\n            return self.transport.sendto(encode(ConnectResponse(0, req.transaction_id, connection_id)), addr)\n        elif action == 1:\n            req = decode(AnnounceRequest, data)\n            if req.connection_id not in self.known_conns:\n                resp = encode(ErrorResponse(3, req.transaction_id, b'Connection ID missmatch.\\x00'))\n            else:\n                compact_address = encode_peer(addr[0], req.port)\n                if req.event != 3:\n                    self.add_peer(req.info_hash, addr[0], req.port)\n                elif compact_address in self.peers.get(req.info_hash, []):\n                    self.peers[req.info_hash].remove(compact_address)\n                peers = [decode(CompactIPv4Peer, peer) for peer in self.peers[req.info_hash]]\n                resp = encode(AnnounceResponse(1, req.transaction_id, 1700, 0, len(peers), peers))\n            return self.transport.sendto(resp, addr)\n\n\ndef encode_peer(ip_address: str, port: int):\n    compact_ip = reduce(lambda buff, x: buff + bytearray([int(x)]), ip_address.split('.'), bytearray())\n    return compact_ip + port.to_bytes(2, \"big\", signed=False)\n"
  },
  {
    "path": "lbry/utils.py",
    "content": "import base64\nimport codecs\nimport datetime\nimport random\nimport socket\nimport time\nimport string\nimport sys\nimport json\nimport typing\nimport asyncio\nimport ssl\nimport logging\nimport ipaddress\nimport contextlib\nimport functools\nimport collections\nimport hashlib\nimport pkg_resources\n\nimport certifi\nimport aiohttp\nfrom prometheus_client import Counter\nfrom lbry.schema.claim import Claim\n\n\nlog = logging.getLogger(__name__)\n\n\n# defining these time functions here allows for easier overriding in testing\ndef now():\n    return datetime.datetime.now()\n\n\ndef utcnow():\n    return datetime.datetime.utcnow()\n\n\ndef isonow():\n    \"\"\"Return utc now in isoformat with timezone\"\"\"\n    return utcnow().isoformat() + 'Z'\n\n\ndef today():\n    return datetime.datetime.today()\n\n\ndef timedelta(**kwargs):\n    return datetime.timedelta(**kwargs)\n\n\ndef datetime_obj(*args, **kwargs):\n    return datetime.datetime(*args, **kwargs)\n\n\ndef get_lbry_hash_obj():\n    return hashlib.sha384()\n\n\ndef generate_id(num=None):\n    h = get_lbry_hash_obj()\n    if num is not None:\n        h.update(str(num).encode())\n    else:\n        h.update(str(random.getrandbits(512)).encode())\n    return h.digest()\n\n\ndef version_is_greater_than(version_a, version_b):\n    \"\"\"Returns True if version a is more recent than version b\"\"\"\n    return pkg_resources.parse_version(version_a) > pkg_resources.parse_version(version_b)\n\n\ndef rot13(some_str):\n    return codecs.encode(some_str, 'rot_13')\n\n\ndef deobfuscate(obfustacated):\n    return base64.b64decode(rot13(obfustacated)).decode()\n\n\ndef obfuscate(plain):\n    return rot13(base64.b64encode(plain).decode())\n\n\ndef check_connection(server=\"lbry.com\", port=80, timeout=5) -> bool:\n    \"\"\"Attempts to open a socket to server:port and returns True if successful.\"\"\"\n    log.debug('Checking connection to %s:%s', server, port)\n    try:\n        server = socket.gethostbyname(server)\n        socket.create_connection((server, port), timeout).close()\n        return True\n    except (socket.gaierror, socket.herror):\n        log.debug(\"Failed to connect to %s:%s. Unable to resolve domain. Trying to bypass DNS\",\n                  server, port)\n        try:\n            server = \"8.8.8.8\"\n            port = 53\n            socket.create_connection((server, port), timeout).close()\n            return True\n        except OSError:\n            return False\n    except OSError:\n        return False\n\n\ndef random_string(length=10, chars=string.ascii_lowercase):\n    return ''.join([random.choice(chars) for _ in range(length)])\n\n\ndef short_hash(hash_str):\n    return hash_str[:6]\n\n\ndef get_sd_hash(stream_info):\n    if not stream_info:\n        return None\n    if isinstance(stream_info, Claim):\n        return stream_info.stream.source.sd_hash\n    result = stream_info.get('claim', {}).\\\n        get('value', {}).\\\n        get('stream', {}).\\\n        get('source', {}).\\\n        get('source')\n    if not result:\n        log.warning(\"Unable to get sd_hash\")\n    return result\n\n\ndef json_dumps_pretty(obj, **kwargs):\n    return json.dumps(obj, sort_keys=True, indent=2, separators=(',', ': '), **kwargs)\n\ntry:\n    # the standard contextlib.aclosing() is available in 3.10+\n    from contextlib import aclosing  # pylint: disable=unused-import\nexcept ImportError:\n    @contextlib.asynccontextmanager\n    async def aclosing(thing):\n        try:\n            yield thing\n        finally:\n            await thing.aclose()\n\ndef async_timed_cache(duration: int):\n    def wrapper(func):\n        cache: typing.Dict[typing.Tuple,\n                           typing.Tuple[typing.Any, float]] = {}\n\n        @functools.wraps(func)\n        async def _inner(*args, **kwargs) -> typing.Any:\n            loop = asyncio.get_running_loop()\n            time_now = loop.time()\n            key = (args, tuple(kwargs.items()))\n            if key in cache and (time_now - cache[key][1] < duration):\n                return cache[key][0]\n            to_cache = await func(*args, **kwargs)\n            cache[key] = to_cache, time_now\n            return to_cache\n        return _inner\n    return wrapper\n\n\ndef cache_concurrent(async_fn):\n    \"\"\"\n    When the decorated function has concurrent calls made to it with the same arguments, only run it once\n    \"\"\"\n    cache: typing.Dict = {}\n\n    @functools.wraps(async_fn)\n    async def wrapper(*args, **kwargs):\n        key = (args, tuple(kwargs.items()))\n        cache[key] = cache.get(key) or asyncio.create_task(async_fn(*args, **kwargs))\n        try:\n            return await cache[key]\n        finally:\n            cache.pop(key, None)\n\n    return wrapper\n\n\n@async_timed_cache(300)\nasync def resolve_host(url: str, port: int, proto: str) -> str:\n    if proto not in ['udp', 'tcp']:\n        raise Exception(\"invalid protocol\")\n    if url.lower() == 'localhost':\n        return '127.0.0.1'\n    try:\n        if ipaddress.ip_address(url):\n            return url\n    except ValueError:\n        pass\n    loop = asyncio.get_running_loop()\n    return (await loop.getaddrinfo(\n        url, port,\n        proto=socket.IPPROTO_TCP if proto == 'tcp' else socket.IPPROTO_UDP,\n        type=socket.SOCK_STREAM if proto == 'tcp' else socket.SOCK_DGRAM,\n        family=socket.AF_INET\n    ))[0][4][0]\n\n\nclass LRUCacheWithMetrics:\n    __slots__ = [\n        'capacity',\n        'cache',\n        '_track_metrics',\n        'hits',\n        'misses'\n    ]\n\n    def __init__(self, capacity: int, metric_name: typing.Optional[str] = None, namespace: str = \"daemon_cache\"):\n        self.capacity = capacity\n        self.cache = collections.OrderedDict()\n        if metric_name is None:\n            self._track_metrics = False\n            self.hits = self.misses = None\n        else:\n            self._track_metrics = True\n            try:\n                self.hits = Counter(\n                    f\"{metric_name}_cache_hit_count\", \"Number of cache hits\", namespace=namespace\n                )\n                self.misses = Counter(\n                    f\"{metric_name}_cache_miss_count\", \"Number of cache misses\", namespace=namespace\n                )\n            except ValueError as err:\n                log.debug(\"failed to set up prometheus %s_cache_miss_count metric: %s\", metric_name, err)\n                self._track_metrics = False\n                self.hits = self.misses = None\n\n    def get(self, key, default=None):\n        try:\n            value = self.cache.pop(key)\n            if self._track_metrics:\n                self.hits.inc()\n        except KeyError:\n            if self._track_metrics:\n                self.misses.inc()\n            return default\n        self.cache[key] = value\n        return value\n\n    def set(self, key, value):\n        try:\n            self.cache.pop(key)\n        except KeyError:\n            if len(self.cache) >= self.capacity:\n                self.cache.popitem(last=False)\n        self.cache[key] = value\n\n    def clear(self):\n        self.cache.clear()\n\n    def pop(self, key):\n        return self.cache.pop(key)\n\n    def __setitem__(self, key, value):\n        return self.set(key, value)\n\n    def __getitem__(self, item):\n        return self.get(item)\n\n    def __contains__(self, item) -> bool:\n        return item in self.cache\n\n    def __len__(self):\n        return len(self.cache)\n\n    def __delitem__(self, key):\n        self.cache.pop(key)\n\n    def __del__(self):\n        self.clear()\n\n\nclass LRUCache:\n    __slots__ = [\n        'capacity',\n        'cache'\n    ]\n\n    def __init__(self, capacity: int):\n        self.capacity = capacity\n        self.cache = collections.OrderedDict()\n\n    def get(self, key, default=None):\n        try:\n            value = self.cache.pop(key)\n        except KeyError:\n            return default\n        self.cache[key] = value\n        return value\n\n    def set(self, key, value):\n        try:\n            self.cache.pop(key)\n        except KeyError:\n            if len(self.cache) >= self.capacity:\n                self.cache.popitem(last=False)\n        self.cache[key] = value\n\n    def items(self):\n        return self.cache.items()\n\n    def clear(self):\n        self.cache.clear()\n\n    def pop(self, key, default=None):\n        return self.cache.pop(key, default)\n\n    def __setitem__(self, key, value):\n        return self.set(key, value)\n\n    def __getitem__(self, item):\n        return self.get(item)\n\n    def __contains__(self, item) -> bool:\n        return item in self.cache\n\n    def __len__(self):\n        return len(self.cache)\n\n    def __delitem__(self, key):\n        self.cache.pop(key)\n\n    def __del__(self):\n        self.clear()\n\n\ndef lru_cache_concurrent(cache_size: typing.Optional[int] = None,\n                         override_lru_cache: typing.Optional[LRUCacheWithMetrics] = None):\n    if not cache_size and override_lru_cache is None:\n        raise ValueError(\"invalid cache size\")\n    concurrent_cache = {}\n    lru_cache = override_lru_cache if override_lru_cache is not None else LRUCacheWithMetrics(cache_size)\n\n    def wrapper(async_fn):\n\n        @functools.wraps(async_fn)\n        async def _inner(*args, **kwargs):\n            key = (args, tuple(kwargs.items()))\n            if key in lru_cache:\n                return lru_cache.get(key)\n\n            concurrent_cache[key] = concurrent_cache.get(key) or asyncio.create_task(async_fn(*args, **kwargs))\n\n            try:\n                result = await concurrent_cache[key]\n                lru_cache.set(key, result)\n                return result\n            finally:\n                concurrent_cache.pop(key, None)\n        return _inner\n    return wrapper\n\n\ndef get_ssl_context() -> ssl.SSLContext:\n    return ssl.create_default_context(\n        purpose=ssl.Purpose.CLIENT_AUTH, capath=certifi.where()\n    )\n\n\n@contextlib.asynccontextmanager\nasync def aiohttp_request(method, url, **kwargs) -> typing.AsyncContextManager[aiohttp.ClientResponse]:\n    async with aiohttp.ClientSession() as session:\n        async with session.request(method, url, **kwargs) as response:\n            yield response\n\n\n# the ipaddress module does not show these subnets as reserved\nCARRIER_GRADE_NAT_SUBNET = ipaddress.ip_network('100.64.0.0/10')\nIPV4_TO_6_RELAY_SUBNET = ipaddress.ip_network('192.88.99.0/24')\n\n\ndef is_valid_public_ipv4(address, allow_localhost: bool = False, allow_lan: bool = False):\n    try:\n        parsed_ip = ipaddress.ip_address(address)\n        if parsed_ip.is_loopback and allow_localhost:\n            return True\n        if allow_lan and parsed_ip.is_private:\n            return True\n        if any((parsed_ip.version != 4, parsed_ip.is_unspecified, parsed_ip.is_link_local, parsed_ip.is_loopback,\n                parsed_ip.is_multicast, parsed_ip.is_reserved, parsed_ip.is_private)):\n            return False\n        else:\n            return not any((CARRIER_GRADE_NAT_SUBNET.supernet_of(ipaddress.ip_network(f\"{address}/32\")),\n                            IPV4_TO_6_RELAY_SUBNET.supernet_of(ipaddress.ip_network(f\"{address}/32\"))))\n    except (ipaddress.AddressValueError, ValueError):\n        return False\n\n\nasync def fallback_get_external_ip():  # used if spv servers can't be used for ip detection\n    try:\n        async with aiohttp_request(\"get\", \"https://api.lbry.com/ip\") as resp:\n            response = await resp.json()\n            if response['success']:\n                return response['data']['ip'], None\n    except Exception:\n        return None, None\n\n\nasync def _get_external_ip(default_servers) -> typing.Tuple[typing.Optional[str], typing.Optional[str]]:\n    # used if upnp is disabled or non-functioning\n    from lbry.wallet.udp import SPVStatusClientProtocol  # pylint: disable=C0415\n\n    hostname_to_ip = {}\n    ip_to_hostnames = collections.defaultdict(list)\n\n    async def resolve_spv(server, port):\n        try:\n            server_addr = await resolve_host(server, port, 'udp')\n            hostname_to_ip[server] = (server_addr, port)\n            ip_to_hostnames[(server_addr, port)].append(server)\n        except Exception:\n            log.exception(\"error looking up dns for spv servers\")\n\n    # accumulate the dns results\n    await asyncio.gather(*(resolve_spv(server, port) for (server, port) in default_servers))\n\n    loop = asyncio.get_event_loop()\n    pong_responses = asyncio.Queue()\n    connection = SPVStatusClientProtocol(pong_responses)\n    try:\n        await loop.create_datagram_endpoint(lambda: connection, ('0.0.0.0', 0))\n        # could raise OSError if it cant bind\n        randomized_servers = list(ip_to_hostnames.keys())\n        random.shuffle(randomized_servers)\n        for server in randomized_servers:\n            connection.ping(server)\n            try:\n                _, pong = await asyncio.wait_for(pong_responses.get(), 1)\n                if is_valid_public_ipv4(pong.ip_address):\n                    return pong.ip_address, ip_to_hostnames[server][0]\n            except asyncio.TimeoutError:\n                pass\n        return None, None\n    finally:\n        connection.close()\n\n\nasync def get_external_ip(default_servers) -> typing.Tuple[typing.Optional[str], typing.Optional[str]]:\n    ip_from_spv_servers = await _get_external_ip(default_servers)\n    if not ip_from_spv_servers[1]:\n        return await fallback_get_external_ip()\n    return ip_from_spv_servers\n\n\ndef is_running_from_bundle():\n    # see https://pyinstaller.readthedocs.io/en/stable/runtime-information.html\n    return getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS')\n\n\nclass LockWithMetrics(asyncio.Lock):\n    def __init__(self, acquire_metric, held_time_metric):\n        super().__init__()\n        self._acquire_metric = acquire_metric\n        self._lock_held_time_metric = held_time_metric\n        self._lock_acquired_time = None\n\n    async def acquire(self):\n        start = time.perf_counter()\n        try:\n            return await super().acquire()\n        finally:\n            self._lock_acquired_time = time.perf_counter()\n            self._acquire_metric.observe(self._lock_acquired_time - start)\n\n    def release(self):\n        try:\n            return super().release()\n        finally:\n            self._lock_held_time_metric.observe(time.perf_counter() - self._lock_acquired_time)\n\n\ndef get_colliding_prefix_bits(first_value: bytes, second_value: bytes):\n    \"\"\"\n    Calculates the amount of colliding prefix bits between <first_value> and <second_value>.\n    This is given by the amount of bits that are the same until the first different one (via XOR),\n    starting from the most significant bit to the least significant bit.\n    :param first_value: first value to compare, bigger than size.\n    :param second_value: second value to compare, bigger than size.\n    :return: amount of prefix colliding bits.\n    \"\"\"\n    assert len(first_value) == len(second_value), \"length should be the same\"\n    size = len(first_value) * 8\n    first_value, second_value = int.from_bytes(first_value, \"big\"), int.from_bytes(second_value, \"big\")\n    return size - (first_value ^ second_value).bit_length()\n"
  },
  {
    "path": "lbry/wallet/__init__.py",
    "content": "__lbcd__ = 'lbcd'\n__lbcctl__ = 'lbcctl'\n__lbcwallet__ = 'lbcwallet'\n__lbcd_url__ = (\n    'https://github.com/lbryio/lbcd/releases/download/' +\n    'v0.22.100-rc.0/lbcd_0.22.100-rc.0_TARGET_PLATFORM.tar.gz'\n)\n__lbcwallet_url__ = (\n    'https://github.com/lbryio/lbcwallet/releases/download/' +\n    'v0.13.100-alpha.0/lbcwallet_0.13.100-alpha.0_TARGET_PLATFORM.tar.gz'\n)\n__spvserver__ = 'lbry.wallet.server.coin.LBCRegTest'\n\nfrom lbry.wallet.wallet import Wallet, WalletStorage, TimestampedPreferences, ENCRYPT_ON_DISK\nfrom lbry.wallet.manager import WalletManager\nfrom lbry.wallet.network import Network\nfrom lbry.wallet.ledger import Ledger, RegTestLedger, TestNetLedger, BlockHeightEvent\nfrom lbry.wallet.account import Account, AddressManager, SingleKey, HierarchicalDeterministic, \\\n    DeterministicChannelKeyManager\nfrom lbry.wallet.transaction import Transaction, Output, Input\nfrom lbry.wallet.script import OutputScript, InputScript\nfrom lbry.wallet.database import SQLiteMixin, Database\nfrom lbry.wallet.header import Headers\n"
  },
  {
    "path": "lbry/wallet/account.py",
    "content": "import os\nimport time\nimport json\nimport logging\nimport typing\nimport asyncio\nimport random\nfrom hashlib import sha256\nfrom string import hexdigits\nfrom typing import Type, Dict, Tuple, Optional, Any, List\n\nfrom lbry.error import InvalidPasswordError\nfrom lbry.crypto.crypt import aes_encrypt, aes_decrypt\n\nfrom .bip32 import PrivateKey, PublicKey, KeyPath, from_extended_key_string\nfrom .mnemonic import Mnemonic\nfrom .constants import COIN, TXO_TYPES\nfrom .transaction import Transaction, Input, Output\n\nif typing.TYPE_CHECKING:\n    from .ledger import Ledger\n    from .wallet import Wallet\n\nlog = logging.getLogger(__name__)\n\n\ndef validate_claim_id(claim_id):\n    if not len(claim_id) == 40:\n        raise Exception(\"Incorrect claimid length: %i\" % len(claim_id))\n    if isinstance(claim_id, bytes):\n        claim_id = claim_id.decode('utf-8')\n    if set(claim_id).difference(hexdigits):\n        raise Exception(\"Claim id is not hex encoded\")\n\n\nclass DeterministicChannelKeyManager:\n\n    def __init__(self, account: 'Account'):\n        self.account = account\n        self.last_known = 0\n        self.cache = {}\n        self._private_key: Optional[PrivateKey] = None\n\n    @property\n    def private_key(self):\n        if self._private_key is None:\n            if self.account.private_key is not None:\n                self._private_key = self.account.private_key.child(KeyPath.CHANNEL)\n        return self._private_key\n\n    def maybe_generate_deterministic_key_for_channel(self, txo):\n        if self.private_key is None:\n            return\n        next_private_key = self.private_key.child(self.last_known)\n        public_key = next_private_key.public_key\n        public_key_bytes = public_key.pubkey_bytes\n        if txo.claim.channel.public_key_bytes == public_key_bytes:\n            self.cache[public_key.address] = next_private_key\n            self.last_known += 1\n\n    async def ensure_cache_primed(self):\n        if self.private_key is not None:\n            await self.generate_next_key()\n\n    async def generate_next_key(self) -> PrivateKey:\n        db = self.account.ledger.db\n        while True:\n            next_private_key = self.private_key.child(self.last_known)\n            public_key = next_private_key.public_key\n            self.cache[public_key.address] = next_private_key\n            if not await db.is_channel_key_used(self.account, public_key):\n                return next_private_key\n            self.last_known += 1\n\n    def get_private_key_from_pubkey_hash(self, pubkey_hash) -> PrivateKey:\n        return self.cache.get(pubkey_hash)\n\n\nclass AddressManager:\n\n    name: str\n\n    __slots__ = 'account', 'public_key', 'chain_number', 'address_generator_lock'\n\n    def __init__(self, account, public_key, chain_number):\n        self.account = account\n        self.public_key = public_key\n        self.chain_number = chain_number\n        self.address_generator_lock = asyncio.Lock()\n\n    @classmethod\n    def from_dict(cls, account: 'Account', d: dict) \\\n            -> Tuple['AddressManager', 'AddressManager']:\n        raise NotImplementedError\n\n    @classmethod\n    def to_dict(cls, receiving: 'AddressManager', change: 'AddressManager') -> Dict:\n        d: Dict[str, Any] = {'name': cls.name}\n        receiving_dict = receiving.to_dict_instance()\n        if receiving_dict:\n            d['receiving'] = receiving_dict\n        change_dict = change.to_dict_instance()\n        if change_dict:\n            d['change'] = change_dict\n        return d\n\n    def merge(self, d: dict):\n        pass\n\n    def to_dict_instance(self) -> Optional[dict]:\n        raise NotImplementedError\n\n    def _query_addresses(self, **constraints):\n        return self.account.ledger.db.get_addresses(\n            read_only=constraints.pop(\"read_only\", False),\n            accounts=[self.account],\n            chain=self.chain_number,\n            **constraints\n        )\n\n    def get_private_key(self, index: int) -> PrivateKey:\n        raise NotImplementedError\n\n    def get_public_key(self, index: int) -> PublicKey:\n        raise NotImplementedError\n\n    async def get_max_gap(self):\n        raise NotImplementedError\n\n    async def ensure_address_gap(self):\n        raise NotImplementedError\n\n    def get_address_records(self, only_usable: bool = False, **constraints):\n        raise NotImplementedError\n\n    async def get_addresses(self, only_usable: bool = False, **constraints) -> List[str]:\n        records = await self.get_address_records(only_usable=only_usable, **constraints)\n        return [r['address'] for r in records]\n\n    async def get_or_create_usable_address(self) -> str:\n        async with self.address_generator_lock:\n            addresses = await self.get_addresses(only_usable=True, limit=10)\n        if addresses:\n            return random.choice(addresses)\n        addresses = await self.ensure_address_gap()\n        return addresses[0]\n\n\nclass HierarchicalDeterministic(AddressManager):\n    \"\"\" Implements simple version of Bitcoin Hierarchical Deterministic key management. \"\"\"\n\n    name: str = \"deterministic-chain\"\n\n    __slots__ = 'gap', 'maximum_uses_per_address'\n\n    def __init__(self, account: 'Account', chain: int, gap: int, maximum_uses_per_address: int) -> None:\n        super().__init__(account, account.public_key.child(chain), chain)\n        self.gap = gap\n        self.maximum_uses_per_address = maximum_uses_per_address\n\n    @classmethod\n    def from_dict(cls, account: 'Account', d: dict) -> Tuple[AddressManager, AddressManager]:\n        return (\n            cls(account, KeyPath.RECEIVE, **d.get('receiving', {'gap': 20, 'maximum_uses_per_address': 1})),\n            cls(account, KeyPath.CHANGE, **d.get('change', {'gap': 6, 'maximum_uses_per_address': 1}))\n        )\n\n    def merge(self, d: dict):\n        self.gap = d.get('gap', self.gap)\n        self.maximum_uses_per_address = d.get('maximum_uses_per_address', self.maximum_uses_per_address)\n\n    def to_dict_instance(self):\n        return {'gap': self.gap, 'maximum_uses_per_address': self.maximum_uses_per_address}\n\n    def get_private_key(self, index: int) -> PrivateKey:\n        return self.account.private_key.child(self.chain_number).child(index)\n\n    def get_public_key(self, index: int) -> PublicKey:\n        return self.account.public_key.child(self.chain_number).child(index)\n\n    async def get_max_gap(self) -> int:\n        addresses = await self._query_addresses(order_by=\"n asc\")\n        max_gap = 0\n        current_gap = 0\n        for address in addresses:\n            if address['used_times'] == 0:\n                current_gap += 1\n            else:\n                max_gap = max(max_gap, current_gap)\n                current_gap = 0\n        return max_gap\n\n    async def ensure_address_gap(self) -> List[str]:\n        async with self.address_generator_lock:\n            addresses = await self._query_addresses(limit=self.gap, order_by=\"n desc\")\n\n            existing_gap = 0\n            for address in addresses:\n                if address['used_times'] == 0:\n                    existing_gap += 1\n                else:\n                    break\n\n            if existing_gap == self.gap:\n                return []\n\n            start = addresses[0]['pubkey'].n+1 if addresses else 0\n            end = start + (self.gap - existing_gap)\n            new_keys = await self._generate_keys(start, end-1)\n            await self.account.ledger.announce_addresses(self, new_keys)\n            return new_keys\n\n    async def _generate_keys(self, start: int, end: int) -> List[str]:\n        if not self.address_generator_lock.locked():\n            raise RuntimeError('Should not be called outside of address_generator_lock.')\n        keys = [self.public_key.child(index) for index in range(start, end+1)]\n        await self.account.ledger.db.add_keys(self.account, self.chain_number, keys)\n        return [key.address for key in keys]\n\n    def get_address_records(self, only_usable: bool = False, **constraints):\n        if only_usable:\n            constraints['used_times__lt'] = self.maximum_uses_per_address\n        if 'order_by' not in constraints:\n            constraints['order_by'] = \"used_times asc, n asc\"\n        return self._query_addresses(**constraints)\n\n\nclass SingleKey(AddressManager):\n    \"\"\" Single Key address manager always returns the same address for all operations. \"\"\"\n\n    name: str = \"single-address\"\n\n    __slots__ = ()\n\n    @classmethod\n    def from_dict(cls, account: 'Account', d: dict) \\\n            -> Tuple[AddressManager, AddressManager]:\n        same_address_manager = cls(account, account.public_key, KeyPath.RECEIVE)\n        return same_address_manager, same_address_manager\n\n    def to_dict_instance(self):\n        return None\n\n    def get_private_key(self, index: int) -> PrivateKey:\n        return self.account.private_key\n\n    def get_public_key(self, index: int) -> PublicKey:\n        return self.account.public_key\n\n    async def get_max_gap(self) -> int:\n        return 0\n\n    async def ensure_address_gap(self) -> List[str]:\n        async with self.address_generator_lock:\n            exists = await self.get_address_records()\n            if not exists:\n                await self.account.ledger.db.add_keys(self.account, self.chain_number, [self.public_key])\n                new_keys = [self.public_key.address]\n                await self.account.ledger.announce_addresses(self, new_keys)\n                return new_keys\n            return []\n\n    def get_address_records(self, only_usable: bool = False, **constraints):\n        return self._query_addresses(**constraints)\n\n\nclass Account:\n\n    address_generators: Dict[str, Type[AddressManager]] = {\n        SingleKey.name: SingleKey,\n        HierarchicalDeterministic.name: HierarchicalDeterministic,\n    }\n\n    def __init__(self, ledger: 'Ledger', wallet: 'Wallet', name: str,\n                 seed: str, private_key_string: str, encrypted: bool,\n                 private_key: Optional[PrivateKey], public_key: PublicKey,\n                 address_generator: dict, modified_on: float, channel_keys: dict) -> None:\n        self.ledger = ledger\n        self.wallet = wallet\n        self.id = public_key.address\n        self.name = name\n        self.seed = seed\n        self.modified_on = modified_on\n        self.private_key_string = private_key_string\n        self.init_vectors: Dict[str, bytes] = {}\n        self.encrypted = encrypted\n        self.private_key: Optional[PrivateKey] = private_key\n        self.public_key: PublicKey = public_key\n        generator_name = address_generator.get('name', HierarchicalDeterministic.name)\n        self.address_generator = self.address_generators[generator_name]\n        self.receiving, self.change = self.address_generator.from_dict(self, address_generator)\n        self.address_managers = {am.chain_number: am for am in (self.receiving, self.change)}\n        self.channel_keys = channel_keys\n        self.deterministic_channel_keys = DeterministicChannelKeyManager(self)\n        ledger.add_account(self)\n        wallet.add_account(self)\n\n    def get_init_vector(self, key) -> Optional[bytes]:\n        init_vector = self.init_vectors.get(key, None)\n        if init_vector is None:\n            init_vector = self.init_vectors[key] = os.urandom(16)\n        return init_vector\n\n    @classmethod\n    def generate(cls, ledger: 'Ledger', wallet: 'Wallet',\n                 name: str = None, address_generator: dict = None):\n        return cls.from_dict(ledger, wallet, {\n            'name': name,\n            'seed': Mnemonic().make_seed(),\n            'address_generator': address_generator or {}\n        })\n\n    @classmethod\n    def get_private_key_from_seed(cls, ledger: 'Ledger', seed: str, password: str):\n        return PrivateKey.from_seed(\n            ledger, Mnemonic.mnemonic_to_seed(seed, password or 'lbryum')\n        )\n\n    @classmethod\n    def keys_from_dict(cls, ledger: 'Ledger', d: dict) \\\n            -> Tuple[str, Optional[PrivateKey], PublicKey]:\n        seed = d.get('seed', '')\n        private_key_string = d.get('private_key', '')\n        private_key = None\n        public_key = None\n        encrypted = d.get('encrypted', False)\n        if not encrypted:\n            if seed:\n                private_key = cls.get_private_key_from_seed(ledger, seed, '')\n                public_key = private_key.public_key\n            elif private_key_string:\n                private_key = from_extended_key_string(ledger, private_key_string)\n                public_key = private_key.public_key\n        if public_key is None:\n            public_key = from_extended_key_string(ledger, d['public_key'])\n        return seed, private_key, public_key\n\n    @classmethod\n    def from_dict(cls, ledger: 'Ledger', wallet: 'Wallet', d: dict):\n        seed, private_key, public_key = cls.keys_from_dict(ledger, d)\n        name = d.get('name')\n        if not name:\n            name = f'Account #{public_key.address}'\n        return cls(\n            ledger=ledger,\n            wallet=wallet,\n            name=name,\n            seed=seed,\n            private_key_string=d.get('private_key', ''),\n            encrypted=d.get('encrypted', False),\n            private_key=private_key,\n            public_key=public_key,\n            address_generator=d.get('address_generator', {}),\n            modified_on=int(d.get('modified_on', time.time())),\n            channel_keys=d.get('certificates', {})\n        )\n\n    def to_dict(self, encrypt_password: str = None, include_channel_keys: bool = True):\n        private_key_string, seed = self.private_key_string, self.seed\n        if not self.encrypted and self.private_key:\n            private_key_string = self.private_key.extended_key_string()\n        if not self.encrypted and encrypt_password:\n            if private_key_string:\n                private_key_string = aes_encrypt(\n                    encrypt_password, private_key_string, self.get_init_vector('private_key')\n                )\n            if seed:\n                seed = aes_encrypt(encrypt_password, self.seed, self.get_init_vector('seed'))\n        d = {\n            'ledger': self.ledger.get_id(),\n            'name': self.name,\n            'seed': seed,\n            'encrypted': bool(self.encrypted or encrypt_password),\n            'private_key': private_key_string,\n            'public_key': self.public_key.extended_key_string(),\n            'address_generator': self.address_generator.to_dict(self.receiving, self.change),\n            'modified_on': self.modified_on\n        }\n        if include_channel_keys:\n            d['certificates'] = self.channel_keys\n        return d\n\n    def merge(self, d: dict):\n        if d.get('modified_on', 0) > self.modified_on:\n            self.name = d['name']\n            self.modified_on = int(d.get('modified_on', time.time()))\n            assert self.address_generator.name == d['address_generator']['name']\n            for chain_name in ('change', 'receiving'):\n                if chain_name in d['address_generator']:\n                    chain_object = getattr(self, chain_name)\n                    chain_object.merge(d['address_generator'][chain_name])\n        self.channel_keys.update(d.get('certificates', {}))\n\n    @property\n    def hash(self) -> bytes:\n        assert not self.encrypted, \"Cannot hash an encrypted account.\"\n        h = sha256(json.dumps(self.to_dict(include_channel_keys=False)).encode())\n        for cert in sorted(self.channel_keys.keys()):\n            h.update(cert.encode())\n        return h.digest()\n\n    async def get_details(self, show_seed=False, **kwargs):\n        satoshis = await self.get_balance(**kwargs)\n        details = {\n            'id': self.id,\n            'name': self.name,\n            'ledger': self.ledger.get_id(),\n            'coins': round(satoshis/COIN, 2),\n            'satoshis': satoshis,\n            'encrypted': self.encrypted,\n            'public_key': self.public_key.extended_key_string(),\n            'address_generator': self.address_generator.to_dict(self.receiving, self.change)\n        }\n        if show_seed:\n            details['seed'] = self.seed\n        details['certificates'] = len(self.channel_keys)\n        return details\n\n    def decrypt(self, password: str) -> bool:\n        assert self.encrypted, \"Key is not encrypted.\"\n        try:\n            seed = self._decrypt_seed(password)\n        except (ValueError, InvalidPasswordError):\n            return False\n        try:\n            private_key = self._decrypt_private_key_string(password)\n        except (TypeError, ValueError, InvalidPasswordError):\n            return False\n        self.seed = seed\n        self.private_key = private_key\n        self.private_key_string = \"\"\n        self.encrypted = False\n        return True\n\n    def _decrypt_private_key_string(self, password: str) -> Optional[PrivateKey]:\n        if not self.private_key_string:\n            return None\n        private_key_string, self.init_vectors['private_key'] = aes_decrypt(password, self.private_key_string)\n        if not private_key_string:\n            return None\n        return from_extended_key_string(\n            self.ledger, private_key_string\n        )\n\n    def _decrypt_seed(self, password: str) -> str:\n        if not self.seed:\n            return \"\"\n        seed, self.init_vectors['seed'] = aes_decrypt(password, self.seed)\n        if not seed:\n            return \"\"\n        try:\n            Mnemonic().mnemonic_decode(seed)\n        except IndexError:\n            # failed to decode the seed, this either means it decrypted and is invalid\n            # or that we hit an edge case where an incorrect password gave valid padding\n            raise ValueError(\"Failed to decode seed.\")\n        return seed\n\n    def encrypt(self, password: str) -> bool:\n        assert not self.encrypted, \"Key is already encrypted.\"\n        if self.seed:\n            self.seed = aes_encrypt(password, self.seed, self.get_init_vector('seed'))\n        if isinstance(self.private_key, PrivateKey):\n            self.private_key_string = aes_encrypt(\n                password, self.private_key.extended_key_string(), self.get_init_vector('private_key')\n            )\n            self.private_key = None\n        self.encrypted = True\n        return True\n\n    async def ensure_address_gap(self):\n        addresses = []\n        for address_manager in self.address_managers.values():\n            new_addresses = await address_manager.ensure_address_gap()\n            addresses.extend(new_addresses)\n        return addresses\n\n    async def get_addresses(self, read_only=False, **constraints) -> List[str]:\n        rows = await self.ledger.db.select_addresses('address', read_only=read_only, accounts=[self], **constraints)\n        return [r['address'] for r in rows]\n\n    def get_address_records(self, **constraints):\n        return self.ledger.db.get_addresses(accounts=[self], **constraints)\n\n    def get_address_count(self, **constraints):\n        return self.ledger.db.get_address_count(accounts=[self], **constraints)\n\n    def get_private_key(self, chain: int, index: int) -> PrivateKey:\n        assert not self.encrypted, \"Cannot get private key on encrypted wallet account.\"\n        return self.address_managers[chain].get_private_key(index)\n\n    def get_public_key(self, chain: int, index: int) -> PublicKey:\n        return self.address_managers[chain].get_public_key(index)\n\n    def get_balance(self, confirmations=0, include_claims=False, read_only=False, **constraints):\n        if not include_claims:\n            constraints.update({'txo_type__in': (TXO_TYPES['other'], TXO_TYPES['purchase'])})\n        if confirmations > 0:\n            height = self.ledger.headers.height - (confirmations-1)\n            constraints.update({'height__lte': height, 'height__gt': 0})\n        return self.ledger.db.get_balance(accounts=[self], read_only=read_only, **constraints)\n\n    async def get_max_gap(self):\n        change_gap = await self.change.get_max_gap()\n        receiving_gap = await self.receiving.get_max_gap()\n        return {\n            'max_change_gap': change_gap,\n            'max_receiving_gap': receiving_gap,\n        }\n\n    def get_txos(self, **constraints):\n        return self.ledger.get_txos(wallet=self.wallet, accounts=[self], **constraints)\n\n    def get_txo_count(self, **constraints):\n        return self.ledger.get_txo_count(wallet=self.wallet, accounts=[self], **constraints)\n\n    def get_utxos(self, **constraints):\n        return self.ledger.get_utxos(wallet=self.wallet, accounts=[self], **constraints)\n\n    def get_utxo_count(self, **constraints):\n        return self.ledger.get_utxo_count(wallet=self.wallet, accounts=[self], **constraints)\n\n    def get_transactions(self, **constraints):\n        return self.ledger.get_transactions(wallet=self.wallet, accounts=[self], **constraints)\n\n    def get_transaction_count(self, **constraints):\n        return self.ledger.get_transaction_count(wallet=self.wallet, accounts=[self], **constraints)\n\n    async def fund(self, to_account, amount=None, everything=False,\n                   outputs=1, broadcast=False, **constraints):\n        assert self.ledger == to_account.ledger, 'Can only transfer between accounts of the same ledger.'\n        if everything:\n            utxos = await self.get_utxos(**constraints)\n            await self.ledger.reserve_outputs(utxos)\n            tx = await Transaction.create(\n                inputs=[Input.spend(txo) for txo in utxos],\n                outputs=[],\n                funding_accounts=[self],\n                change_account=to_account\n            )\n        elif amount > 0:\n            to_address = await to_account.change.get_or_create_usable_address()\n            to_hash160 = to_account.ledger.address_to_hash160(to_address)\n            tx = await Transaction.create(\n                inputs=[],\n                outputs=[\n                    Output.pay_pubkey_hash(amount//outputs, to_hash160)\n                    for _ in range(outputs)\n                ],\n                funding_accounts=[self],\n                change_account=self\n            )\n        else:\n            raise ValueError('An amount is required.')\n\n        if broadcast:\n            await self.ledger.broadcast(tx)\n        else:\n            await self.ledger.release_tx(tx)\n\n        return tx\n\n    async def generate_channel_private_key(self):\n        return await self.deterministic_channel_keys.generate_next_key()\n\n    def add_channel_private_key(self, private_key: PrivateKey):\n        self.channel_keys[private_key.address] = private_key.to_pem().decode()\n\n    async def get_channel_private_key(self, public_key_bytes) -> PrivateKey:\n        channel_pubkey_hash = self.ledger.public_key_to_address(public_key_bytes)\n        private_key_pem = self.channel_keys.get(channel_pubkey_hash)\n        if private_key_pem:\n            return PrivateKey.from_pem(self.ledger, private_key_pem)\n        return self.deterministic_channel_keys.get_private_key_from_pubkey_hash(channel_pubkey_hash)\n\n    async def maybe_migrate_certificates(self):\n        if not self.channel_keys:\n            return\n        channel_keys = {}\n        for private_key_pem in self.channel_keys.values():\n            if not isinstance(private_key_pem, str):\n                continue\n            if not private_key_pem.startswith(\"-----BEGIN\"):\n                continue\n            private_key = PrivateKey.from_pem(self.ledger, private_key_pem)\n            channel_keys[private_key.address] = private_key_pem\n        if self.channel_keys != channel_keys:\n            self.channel_keys = channel_keys\n            self.wallet.save()\n\n    async def save_max_gap(self):\n        if issubclass(self.address_generator, HierarchicalDeterministic):\n            gap = await self.get_max_gap()\n            gap_changed = False\n            new_receiving_gap = max(20, gap['max_receiving_gap'] + 1)\n            if self.receiving.gap != new_receiving_gap:\n                self.receiving.gap = new_receiving_gap\n                gap_changed = True\n            new_change_gap = max(6, gap['max_change_gap'] + 1)\n            if self.change.gap != new_change_gap:\n                self.change.gap = new_change_gap\n                gap_changed = True\n            if gap_changed:\n                self.wallet.save()\n\n    async def get_detailed_balance(self, confirmations=0, read_only=False):\n        constraints = {}\n        if confirmations > 0:\n            height = self.ledger.headers.height - (confirmations-1)\n            constraints.update({'height__lte': height, 'height__gt': 0})\n        return await self.ledger.db.get_detailed_balance(\n            accounts=[self], read_only=read_only, **constraints\n        )\n\n    def get_transaction_history(self, read_only=False, **constraints):\n        return self.ledger.get_transaction_history(\n            read_only=read_only, wallet=self.wallet, accounts=[self], **constraints\n        )\n\n    def get_transaction_history_count(self, read_only=False, **constraints):\n        return self.ledger.get_transaction_history_count(\n            read_only=read_only, wallet=self.wallet, accounts=[self], **constraints\n        )\n\n    def get_claims(self, **constraints):\n        return self.ledger.get_claims(wallet=self.wallet, accounts=[self], **constraints)\n\n    def get_claim_count(self, **constraints):\n        return self.ledger.get_claim_count(wallet=self.wallet, accounts=[self], **constraints)\n\n    def get_streams(self, **constraints):\n        return self.ledger.get_streams(wallet=self.wallet, accounts=[self], **constraints)\n\n    def get_stream_count(self, **constraints):\n        return self.ledger.get_stream_count(wallet=self.wallet, accounts=[self], **constraints)\n\n    def get_channels(self, **constraints):\n        return self.ledger.get_channels(wallet=self.wallet, accounts=[self], **constraints)\n\n    def get_channel_count(self, **constraints):\n        return self.ledger.get_channel_count(wallet=self.wallet, accounts=[self], **constraints)\n\n    def get_collections(self, **constraints):\n        return self.ledger.get_collections(wallet=self.wallet, accounts=[self], **constraints)\n\n    def get_collection_count(self, **constraints):\n        return self.ledger.get_collection_count(wallet=self.wallet, accounts=[self], **constraints)\n\n    def get_supports(self, **constraints):\n        return self.ledger.get_supports(wallet=self.wallet, accounts=[self], **constraints)\n\n    def get_support_count(self, **constraints):\n        return self.ledger.get_support_count(wallet=self.wallet, accounts=[self], **constraints)\n\n    def get_support_summary(self):\n        return self.ledger.db.get_supports_summary(wallet=self.wallet, accounts=[self])\n\n    async def release_all_outputs(self):\n        await self.ledger.db.release_all_outputs(self)\n"
  },
  {
    "path": "lbry/wallet/bcd_data_stream.py",
    "content": "import struct\nfrom io import BytesIO\n\n\nclass BCDataStream:\n\n    def __init__(self, data=None):\n        self.data = BytesIO(data)\n\n    def reset(self):\n        self.data.seek(0)\n\n    def get_bytes(self):\n        return self.data.getvalue()\n\n    def read(self, size):\n        return self.data.read(size)\n\n    def write(self, data):\n        self.data.write(data)\n\n    def write_many(self, many):\n        self.data.writelines(many)\n\n    def read_string(self):\n        return self.read(self.read_compact_size())\n\n    def write_string(self, s):\n        self.write_compact_size(len(s))\n        self.write(s)\n\n    def read_compact_size(self):\n        size = self.read_uint8()\n        if size < 253:\n            return size\n        if size == 253:\n            return self.read_uint16()\n        if size == 254:\n            return self.read_uint32()\n        if size == 255:\n            return self.read_uint64()\n\n    def write_compact_size(self, size):\n        if size < 253:\n            self.write_uint8(size)\n        elif size <= 0xFFFF:\n            self.write_uint8(253)\n            self.write_uint16(size)\n        elif size <= 0xFFFFFFFF:\n            self.write_uint8(254)\n            self.write_uint32(size)\n        else:\n            self.write_uint8(255)\n            self.write_uint64(size)\n\n    def read_boolean(self):\n        return self.read_uint8() != 0\n\n    def write_boolean(self, val):\n        return self.write_uint8(1 if val else 0)\n\n    int8 = struct.Struct('b')\n    uint8 = struct.Struct('B')\n    int16 = struct.Struct('<h')\n    uint16 = struct.Struct('<H')\n    int32 = struct.Struct('<i')\n    uint32 = struct.Struct('<I')\n    int64 = struct.Struct('<q')\n    uint64 = struct.Struct('<Q')\n\n    def _read_struct(self, fmt):\n        value = self.read(fmt.size)\n        if value:\n            return fmt.unpack(value)[0]\n\n    def read_int8(self):\n        return self._read_struct(self.int8)\n\n    def read_uint8(self):\n        return self._read_struct(self.uint8)\n\n    def read_int16(self):\n        return self._read_struct(self.int16)\n\n    def read_uint16(self):\n        return self._read_struct(self.uint16)\n\n    def read_int32(self):\n        return self._read_struct(self.int32)\n\n    def read_uint32(self):\n        return self._read_struct(self.uint32)\n\n    def read_int64(self):\n        return self._read_struct(self.int64)\n\n    def read_uint64(self):\n        return self._read_struct(self.uint64)\n\n    def write_int8(self, val):\n        self.write(self.int8.pack(val))\n\n    def write_uint8(self, val):\n        self.write(self.uint8.pack(val))\n\n    def write_int16(self, val):\n        self.write(self.int16.pack(val))\n\n    def write_uint16(self, val):\n        self.write(self.uint16.pack(val))\n\n    def write_int32(self, val):\n        self.write(self.int32.pack(val))\n\n    def write_uint32(self, val):\n        self.write(self.uint32.pack(val))\n\n    def write_int64(self, val):\n        self.write(self.int64.pack(val))\n\n    def write_uint64(self, val):\n        self.write(self.uint64.pack(val))\n"
  },
  {
    "path": "lbry/wallet/bip32.py",
    "content": "from asn1crypto.keys import PrivateKeyInfo, ECPrivateKey\nfrom coincurve import PublicKey as cPublicKey, PrivateKey as cPrivateKey\nfrom coincurve.utils import (\n    pem_to_der, lib as libsecp256k1, ffi as libsecp256k1_ffi\n)\nfrom coincurve.ecdsa import CDATA_SIG_LENGTH\n\nfrom lbry.crypto.hash import hmac_sha512, hash160, double_sha256\nfrom lbry.crypto.base58 import Base58\nfrom .util import cachedproperty\n\n\nclass KeyPath:\n    RECEIVE = 0\n    CHANGE = 1\n    CHANNEL = 2\n\n\nclass DerivationError(Exception):\n    \"\"\" Raised when an invalid derivation occurs. \"\"\"\n\n\nclass _KeyBase:\n    \"\"\" A BIP32 Key, public or private. \"\"\"\n\n    def __init__(self, ledger, chain_code, n, depth, parent):\n        if not isinstance(chain_code, (bytes, bytearray)):\n            raise TypeError('chain code must be raw bytes')\n        if len(chain_code) != 32:\n            raise ValueError('invalid chain code')\n        if not 0 <= n < 1 << 32:\n            raise ValueError('invalid child number')\n        if not 0 <= depth < 256:\n            raise ValueError('invalid depth')\n        if parent is not None:\n            if not isinstance(parent, type(self)):\n                raise TypeError('parent key has bad type')\n        self.ledger = ledger\n        self.chain_code = chain_code\n        self.n = n\n        self.depth = depth\n        self.parent = parent\n\n    def _hmac_sha512(self, msg):\n        \"\"\" Use SHA-512 to provide an HMAC, returned as a pair of 32-byte objects. \"\"\"\n        hmac = hmac_sha512(self.chain_code, msg)\n        return hmac[:32], hmac[32:]\n\n    def _extended_key(self, ver_bytes, raw_serkey):\n        \"\"\" Return the 78-byte extended key given prefix version bytes and serialized key bytes. \"\"\"\n        if not isinstance(ver_bytes, (bytes, bytearray)):\n            raise TypeError('ver_bytes must be raw bytes')\n        if len(ver_bytes) != 4:\n            raise ValueError('ver_bytes must have length 4')\n        if not isinstance(raw_serkey, (bytes, bytearray)):\n            raise TypeError('raw_serkey must be raw bytes')\n        if len(raw_serkey) != 33:\n            raise ValueError('raw_serkey must have length 33')\n\n        return (\n            ver_bytes + bytes((self.depth,))\n            + self.parent_fingerprint() + self.n.to_bytes(4, 'big')\n            + self.chain_code + raw_serkey\n        )\n\n    def identifier(self):\n        raise NotImplementedError\n\n    def extended_key(self):\n        raise NotImplementedError\n\n    def fingerprint(self):\n        \"\"\" Return the key's fingerprint as 4 bytes. \"\"\"\n        return self.identifier()[:4]\n\n    def parent_fingerprint(self):\n        \"\"\" Return the parent key's fingerprint as 4 bytes. \"\"\"\n        return self.parent.fingerprint() if self.parent else bytes((0,)*4)\n\n    def extended_key_string(self):\n        \"\"\" Return an extended key as a base58 string. \"\"\"\n        return Base58.encode_check(self.extended_key())\n\n\nclass PublicKey(_KeyBase):\n    \"\"\" A BIP32 public key. \"\"\"\n\n    def __init__(self, ledger, pubkey, chain_code, n, depth, parent=None):\n        super().__init__(ledger, chain_code, n, depth, parent)\n        if isinstance(pubkey, cPublicKey):\n            self.verifying_key = pubkey\n        else:\n            self.verifying_key = self._verifying_key_from_pubkey(pubkey)\n\n    @classmethod\n    def from_compressed(cls, public_key_bytes, ledger=None) -> 'PublicKey':\n        return cls(ledger, public_key_bytes, bytes((0,)*32), 0, 0)\n\n    @classmethod\n    def _verifying_key_from_pubkey(cls, pubkey):\n        \"\"\" Converts a 33-byte compressed pubkey into an coincurve.PublicKey object. \"\"\"\n        if not isinstance(pubkey, (bytes, bytearray)):\n            raise TypeError('pubkey must be raw bytes')\n        if len(pubkey) != 33:\n            raise ValueError('pubkey must be 33 bytes')\n        if pubkey[0] not in (2, 3):\n            raise ValueError('invalid pubkey prefix byte')\n        return cPublicKey(pubkey)\n\n    @cachedproperty\n    def pubkey_bytes(self):\n        \"\"\" Return the compressed public key as 33 bytes. \"\"\"\n        return self.verifying_key.format(True)\n\n    @cachedproperty\n    def address(self):\n        \"\"\" The public key as a P2PKH address. \"\"\"\n        return self.ledger.public_key_to_address(self.pubkey_bytes)\n\n    def ec_point(self):\n        return self.verifying_key.point()\n\n    def child(self, n: int) -> 'PublicKey':\n        \"\"\" Return the derived child extended pubkey at index N. \"\"\"\n        if not 0 <= n < (1 << 31):\n            raise ValueError('invalid BIP32 public key child number')\n\n        msg = self.pubkey_bytes + n.to_bytes(4, 'big')\n        L_b, R_b = self._hmac_sha512(msg)  # pylint: disable=invalid-name\n        derived_key = self.verifying_key.add(L_b)\n        return PublicKey(self.ledger, derived_key, R_b, n, self.depth + 1, self)\n\n    def identifier(self):\n        \"\"\" Return the key's identifier as 20 bytes. \"\"\"\n        return hash160(self.pubkey_bytes)\n\n    def extended_key(self):\n        \"\"\" Return a raw extended public key. \"\"\"\n        return self._extended_key(\n            self.ledger.extended_public_key_prefix,\n            self.pubkey_bytes\n        )\n\n    def verify(self, signature, digest) -> bool:\n        \"\"\" Verify that a signature is valid for a 32 byte digest. \"\"\"\n\n        if len(signature) != 64:\n            raise ValueError('Signature must be 64 bytes long.')\n\n        if len(digest) != 32:\n            raise ValueError('Digest must be 32 bytes long.')\n\n        key = self.verifying_key\n\n        raw_signature = libsecp256k1_ffi.new('secp256k1_ecdsa_signature *')\n\n        parsed = libsecp256k1.secp256k1_ecdsa_signature_parse_compact(\n            key.context.ctx, raw_signature, signature\n        )\n        assert parsed == 1\n\n        normalized_signature = libsecp256k1_ffi.new('secp256k1_ecdsa_signature *')\n\n        libsecp256k1.secp256k1_ecdsa_signature_normalize(\n            key.context.ctx, normalized_signature, raw_signature\n        )\n\n        verified = libsecp256k1.secp256k1_ecdsa_verify(\n            key.context.ctx, normalized_signature, digest, key.public_key\n        )\n\n        return bool(verified)\n\n\nclass PrivateKey(_KeyBase):\n    \"\"\"A BIP32 private key.\"\"\"\n\n    HARDENED = 1 << 31\n\n    def __init__(self, ledger, privkey, chain_code, n, depth, parent=None):\n        super().__init__(ledger, chain_code, n, depth, parent)\n        if isinstance(privkey, cPrivateKey):\n            self.signing_key = privkey\n        else:\n            self.signing_key = self._signing_key_from_privkey(privkey)\n\n    @classmethod\n    def _signing_key_from_privkey(cls, private_key):\n        \"\"\" Converts a 32-byte private key into an coincurve.PrivateKey object. \"\"\"\n        return cPrivateKey.from_int(PrivateKey._private_key_secret_exponent(private_key))\n\n    @classmethod\n    def _private_key_secret_exponent(cls, private_key):\n        \"\"\" Return the private key as a secret exponent if it is a valid private key. \"\"\"\n        if not isinstance(private_key, (bytes, bytearray)):\n            raise TypeError('private key must be raw bytes')\n        if len(private_key) != 32:\n            raise ValueError('private key must be 32 bytes')\n        return int.from_bytes(private_key, 'big')\n\n    @classmethod\n    def from_seed(cls, ledger, seed) -> 'PrivateKey':\n        # This hard-coded message string seems to be coin-independent...\n        hmac = hmac_sha512(b'Bitcoin seed', seed)\n        privkey, chain_code = hmac[:32], hmac[32:]\n        return cls(ledger, privkey, chain_code, 0, 0)\n\n    @classmethod\n    def from_pem(cls, ledger, pem) -> 'PrivateKey':\n        der = pem_to_der(pem.encode())\n        try:\n            key_int = ECPrivateKey.load(der).native['private_key']\n        except ValueError:\n            key_int = PrivateKeyInfo.load(der).native['private_key']['private_key']\n        private_key = cPrivateKey.from_int(key_int)\n        return cls(ledger, private_key, bytes((0,)*32), 0, 0)\n\n    @classmethod\n    def from_bytes(cls, ledger, key_bytes) -> 'PrivateKey':\n        return cls(ledger, cPrivateKey(key_bytes), bytes((0,)*32), 0, 0)\n\n    @cachedproperty\n    def private_key_bytes(self):\n        \"\"\" Return the serialized private key (no leading zero byte). \"\"\"\n        return self.signing_key.secret\n\n    @cachedproperty\n    def public_key(self) -> PublicKey:\n        \"\"\" Return the corresponding extended public key. \"\"\"\n        verifying_key = self.signing_key.public_key\n        parent_pubkey = self.parent.public_key if self.parent else None\n        return PublicKey(\n            self.ledger, verifying_key, self.chain_code,\n            self.n, self.depth, parent_pubkey\n        )\n\n    def ec_point(self):\n        return self.public_key.ec_point()\n\n    def secret_exponent(self):\n        \"\"\" Return the private key as a secret exponent. \"\"\"\n        return self.signing_key.to_int()\n\n    def wif(self):\n        \"\"\" Return the private key encoded in Wallet Import Format. \"\"\"\n        return self.ledger.private_key_to_wif(self.private_key_bytes)\n\n    @property\n    def address(self):\n        \"\"\" The public key as a P2PKH address. \"\"\"\n        return self.public_key.address\n\n    def child(self, n) -> 'PrivateKey':\n        \"\"\" Return the derived child extended private key at index N.\"\"\"\n        if not 0 <= n < (1 << 32):\n            raise ValueError('invalid BIP32 private key child number')\n\n        if n >= self.HARDENED:\n            serkey = b'\\0' + self.private_key_bytes\n        else:\n            serkey = self.public_key.pubkey_bytes\n\n        msg = serkey + n.to_bytes(4, 'big')\n        L_b, R_b = self._hmac_sha512(msg)  # pylint: disable=invalid-name\n        derived_key = self.signing_key.add(L_b)\n        return PrivateKey(self.ledger, derived_key, R_b, n, self.depth + 1, self)\n\n    def sign(self, data):\n        \"\"\" Produce a signature for piece of data by double hashing it and signing the hash. \"\"\"\n        return self.signing_key.sign(data, hasher=double_sha256)\n\n    def sign_compact(self, digest):\n        \"\"\" Produce a compact signature. \"\"\"\n        key = self.signing_key\n\n        signature = libsecp256k1_ffi.new('secp256k1_ecdsa_signature *')\n        signed = libsecp256k1.secp256k1_ecdsa_sign(\n            key.context.ctx, signature, digest, key.secret,\n            libsecp256k1_ffi.NULL, libsecp256k1_ffi.NULL\n        )\n\n        if not signed:\n            raise ValueError('The private key was invalid.')\n\n        serialized = libsecp256k1_ffi.new('unsigned char[%d]' % CDATA_SIG_LENGTH)\n        compacted = libsecp256k1.secp256k1_ecdsa_signature_serialize_compact(\n            key.context.ctx, serialized, signature\n        )\n        if compacted != 1:\n            raise ValueError('The signature could not be compacted.')\n\n        return bytes(libsecp256k1_ffi.buffer(serialized, CDATA_SIG_LENGTH))\n\n    def identifier(self):\n        \"\"\"Return the key's identifier as 20 bytes.\"\"\"\n        return self.public_key.identifier()\n\n    def extended_key(self):\n        \"\"\"Return a raw extended private key.\"\"\"\n        return self._extended_key(\n            self.ledger.extended_private_key_prefix,\n            b'\\0' + self.private_key_bytes\n        )\n\n    def to_pem(self):\n        return self.signing_key.to_pem()\n\n\ndef _from_extended_key(ledger, ekey):\n    \"\"\"Return a PublicKey or PrivateKey from an extended key raw bytes.\"\"\"\n    if not isinstance(ekey, (bytes, bytearray)):\n        raise TypeError('extended key must be raw bytes')\n    if len(ekey) != 78:\n        raise ValueError('extended key must have length 78')\n\n    depth = ekey[4]\n    n = int.from_bytes(ekey[9:13], 'big')\n    chain_code = ekey[13:45]\n\n    if ekey[:4] == ledger.extended_public_key_prefix:\n        pubkey = ekey[45:]\n        key = PublicKey(ledger, pubkey, chain_code, n, depth)\n    elif ekey[:4] == ledger.extended_private_key_prefix:\n        if ekey[45] != 0:\n            raise ValueError('invalid extended private key prefix byte')\n        privkey = ekey[46:]\n        key = PrivateKey(ledger, privkey, chain_code, n, depth)\n    else:\n        raise ValueError('version bytes unrecognised')\n\n    return key\n\n\ndef from_extended_key_string(ledger, ekey_str):\n    \"\"\"Given an extended key string, such as\n\n    xpub6BsnM1W2Y7qLMiuhi7f7dbAwQZ5Cz5gYJCRzTNainXzQXYjFwtuQXHd\n    3qfi3t3KJtHxshXezfjft93w4UE7BGMtKwhqEHae3ZA7d823DVrL\n\n    return a PublicKey or PrivateKey.\n    \"\"\"\n    return _from_extended_key(ledger, Base58.decode_check(ekey_str))\n"
  },
  {
    "path": "lbry/wallet/checkpoints.py",
    "content": "HASHES = {\n    0: 'bf3ff54138625c56737509f080e7e7f3c55972f0f80e684f8e25d2ad83bedbe2',\n    1000: '4ec1f9aebc8f7f75d5d05430d1512e38598188d56a8b51510c9e47656c4ffad9',\n    2000: '5d5965e43d187b6f8b35b4be51099d9ec6f7b0446dac778f30040b8371b621a2',\n    3000: 'd429f69a9dd890f7d9827232a348fe5120371ed402baf53c383e860081f6706e',\n    4000: 'd18fef650f23032e4e21299b8b71127f53ff6d2114c3eac4592a71f550dc5071',\n    5000: '12c1dd1b9cda29eb4448e619a4099c7bb6612159e1cd9765fbbabeee50f1daff',\n    6000: 'ebda54cab7979bc60f46cab8d36b6fcea31a44c38d6e59bdcb582054b6885cf8',\n    7000: 'd2be08791053336ae9bd322ee5b97e9920764858f173a9e0e8fb54ee5d8bad0e',\n    8000: '94583639d9e2ce5eac78c08ef662190830263ea13468453b0a8492c3aca562fe',\n    9000: 'a6ebb24a3ec9bb3fbdb9d9525729513dd705fe85093c33a07bbfe34ba1f1eae0',\n    10000: 'a5d32c6cd43097f3725e36dd55d08ec04cbe62e40d1fb3b798167cc9c1fa1ce4',\n    11000: 'b5cf8df354bc26e8b0315e8038564a9a7fc1faa6ad75e67819b7eafbcfdcb145',\n    12000: 'ae7b0e5106299e43b78f337fe15e85c27f88d5bcf618a21c03ce43c094dde909',\n    13000: 'b57dfe631571b654e11fb4295aecfc11f612664d22d64a9690f64c0b3ec128bf',\n    14000: '76e55cc1c38d9a6a349c9a42ae0b748d0d8b27ebb30539be8bb92e71e7abca17',\n    15000: '5d33cbc872acb824a89ef601aa3c918e7f4dbb8d5be509a400fa41b03f7fe103',\n    16000: '25760f6a6bfb58104d02e60efa62ba5ae141bbac1f65d944a667ad66fbef15a9',\n    17000: 'ad37d98bb272aee3a950679cdd4cc203b1c129bb3a03fe74223ff858badfcc50',\n    18000: '9eac34fc453b97b1c62228740efcae4ef910b0bea26c322e17e3474b81713b0d',\n    19000: '06783429c8f50cc6495da29d38c86c3873800cf1eceffedfffe2a73f116a39d3',\n    20000: '0237c620f7f68d9ae9852d00551c98a938352d2e3d9336c536f1cbf5246fade6',\n    21000: 'c4dbb4eea7195a2cf73de2d49b32447cec9c063051acbd300b26399c9242b0ed',\n    22000: 'd18fea80b659d9690830c2f37c7c9d53f79fb919debb27cc2131044df26d7ff5',\n    23000: '0371254f4698d3c46596c1fd861e2e1e75053beb69c3c4d8ee6702f0992f0026',\n    24000: 'dbc97c3986bb9bb6230321d1fc8b7ca13369033f57b08ac01bb775f7241861fe',\n    25000: 'cfb9baa125e91d02c060940b5b51cce52b495a0b0bd081cd869a7ca45a0e0585',\n    26000: '6e07fecd3d0f72b8cd50a3cdc1e9f624beb3dac9f6356308a1ec295f47d68f3b',\n    27000: '8ad248da3aac7c1db958d0ce8a2d50fde461bbb96a628efa215116d3b74b74e5',\n    28000: '4e8231ede23b397c49f1a97020ab5fe45465936cb6c8cf1c39ade1b38a80547f',\n    29000: '1453b7a4be4076e2c77b94bc35f639c571dc05b225a5f8018a9a60337fb3ef66',\n    30000: '43d779c83032d75e8df09aeb6ab103ad41618777565fabc2e56bf31c3a20285a',\n    31000: 'ea1426f57f75ee781a6e569772944119f5e65c8b110b31a32b61d951c3a1900f',\n    32000: '43d1a3941b2c3033f78040830d39e657f74958d71b3cd0bc728550469cdb5fde',\n    33000: '3b0c0eac231476efed2b9bb9dff206a6438a14946c3b4238bf0af6d03f9a350f',\n    34000: 'a8136e9d6f3f5048ef8f9f56628009f5a87bb28664ba8028ecae0797e5328309',\n    35000: 'd8136e638995aaea03c6b50f1cc0f1891bd107290f12a6ee57e3cae36b7e3374',\n    36000: '06a7692cf5ce9bb71461e5427e6f55bce2904f9d3af4c28e7343fecbf0e336da',\n    37000: '12a1a49c237497295150f99c76d79d8affe9fd720e0cbffa62be80e2bda7f832',\n    38000: 'fcf3e526931b7f7cead47652f12cdf76641032bc545e22aa0dc83bef46a085fa',\n    39000: 'ffc34a198ab1cc13819c0c90b26a8455627ff12868b49ac1e6b5e1a086ed011f',\n    40000: 'ac71a9895999531350cf77a701035b0b59720a39994c990354b9cb1f6ea8eb49',\n    41000: '4fa753f5f2de41ad69ab32c6802eeb352f070d8684c3bd154eeb8a3c17aa6363',\n    42000: '6f0febbddf9248e3df2254a7196ec13f353c7a8049e098f2fcdf920580abffa4',\n    43000: 'e3e98488cb203be9b531e3b5ce2200f4e5655297a4f4534bcf486ad5e26687bf',\n    44000: 'c7c6078e30163204e49d493d506f72f71d68d2a591874328342ca3cca81c87bd',\n    45000: '2a65629529f69c15a6e404ba54b83e024acba9d6c786be554fb14d28951ddf9b',\n    46000: 'cbc148ad661e6fc390746e94cf21df71edcb222b4e8d8f0771237a8e03412f9c',\n    47000: 'd6977f770749b7ce1a624a61f7b2d26d817a27c50a895466ee10f8bd758c029a',\n    48000: 'df05fddcd5ccfab7811e5d7b6cf7b49df71c7e0f77a57ce384936948114bb93c',\n    49000: '9d26a67c06f066229bcdafd55cbe71e2b2518465d6deb618c418b2f229ef513f',\n    50000: '4ceb896d901315e0f3e10f3b1ed8472f5bb5230d4ed6377214b07e425e515f6a',\n    51000: '218019bd8440a8c242e84940119d4eccccd26134a208cbb92cdea3480069f482',\n    52000: 'c15f1538280e71abc1cf3e117c79b0b671a9f6fb46102fea7b9a1fbcad418011',\n    53000: '0ad4d3812a42e23fc0defe7c62dad451266ee21699ade625a5f00b91993850d3',\n    54000: 'f994cff7b8166b0c39a310bb686f043fe9355678ae406a7fa1a74d4bf6f9b849',\n    55000: '6723d30c99fb45513a42a3ffaa50d1b8350e3c8b52c6dc6894303147eba95d55',\n    56000: '947cbea79aabe2051522d9e957d92b86dee2c420c32a83cf6f6d655136731926',\n    57000: '8a153fa31a971e7e304650257bfdbaac230b36a9beebc01a85a7aab9a357a5f2',\n    58000: 'd19a5234ab995cb851d691b2c2c84ae665890621c52794e54c16d6a87f516c7b',\n    59000: 'f9c7a353e62ce57a5e3f983362b08fdbff5e2aaadce325bba7680104a6f51cb7',\n    60000: '9e1d535bd31525803e93e4b4c2346fcdfee7c5cdc4b9fe3032df9295aa85c754',\n    61000: '640387271e7eb0816b9e290dd157f58d1eb54cfc39bc7ecc4486099af2a22b76',\n    62000: '3d36acc4169a238657729df674277c6cf236e21c2bed4e99b8f1a6e82d00bbd6',\n    63000: '6d4201e45595ea160773d63bb1481f78664156de1f4564d9a8c3f61b3cc1a6c9',\n    64000: 'b2cadf2ba39b2d5e688029cbbc68cc6d71337b29861dee24fe941b31be1198a3',\n    65000: 'd8a364d385ef42e53ce88c82355235ee7a27dd3e642849dc22d08a4041695dfb',\n    66000: '7025d4b2d9537002ce2b9ec42c596eb864e30f41c06f02413997b5629d6dce3b',\n    67000: '852a3fecfb3a77bcd6d2604170cbe9646d9b889c90d234eddc8f9d112777fe41',\n    68000: '38956213012f66a89377f4914db838faa6738732f7a7a865ebc5777f2f126b73',\n    69000: '4c89b07d23727d4bd1a080bcb9a22dcdb45c904f4c9866153a000056d7ec1546',\n    70000: 'e4e7fbf0d17b5f933deba12e045ea23eabfa6f7dbdc633e5c3d3ef8ddd066b47',\n    71000: '1f07c36e82f8e7582ec9df23ddc90715df45978b6aa0a2f17c08ee14cbb23000',\n    72000: 'c59c660b5aca2fa3364524ef43a4fa93fc3f16df777427cf1d810f65ce42e4d2',\n    73000: '83a05ac2fcae8c5d50a90df308f641616fa9b9fb553763c874b764a73def2f95',\n    74000: '448d209c032a2c6f5330481bf34fcf0bf1ea2c47f43fa5f82cf42e0d8f4d2b20',\n    75000: '5627eb3491e1154ad2c90094da85dc7dbfaf89d3765848db1ed915b7e26dde58',\n    76000: 'b1417b1b2360e24fe42345508ba0a333f4bd76f48c959b8bb40a99bf34ec5ba0',\n    77000: '250aca2bc4e6da7a8e1cb2e4deb0396aee7c317177b099d7dab9731b4c0f7573',\n    78000: 'dbfb1dbe80a5db06c9dcc645c5279eaf40559f5e04df0e64b696e80aba5659e0',\n    79000: 'a7183a545925164b5a9882810ae117d0f3df9b3ed55885fe74ebea284bec484f',\n    80000: '5b4a7bfd9b5394e2759daecfe472ea6e92b281a7821b106314b2b2e0facbaab8',\n    81000: 'b7075fe530b44e4f5895521b3cf99a79fe23d8bedde0027d6e7924ca93793eab',\n    82000: '6a1fa75b896d799ae84046cba7cd942dad47ec04bf50a85cd1b2bb15861995e1',\n    83000: 'aa5f4caf970433e90e2e45f9524ab9a6e22281505ada4c206ecf4672486240e3',\n    84000: '103732d12ef792a1adbe8f295ca6abd008867323ee996698b2650bdc1bb8d06b',\n    85000: 'a214c16ffdc505d6caba7bd0c2bae766bb19f7eb4d436cfc037e93783afae7e8',\n    86000: 'f54226ae9a6814a345968b5c2982adf638b995ff50e27de1890b3760ead12158',\n    87000: '26546212a0720cab988194432f4fa7c3e47caf0fb8e31efdd1ee93c3ad056868',\n    88000: '48f7d47bc443bdbc6a37ffe8f1d0d91de16d4a470d03655692de8a04f84c2561',\n    89000: '005bac455ff093a39b907af0441897e7a9ceb19eaf51d9dd9ec2b5ea43ff6d57',\n    90000: 'b482f10315170e6bd280325e180ce37b0423b685baa06d33586ee659649b71a7',\n    91000: '56db4d5d6c460fa76d492d9900c02e8fa99bfe97c7d9fa2a75bf115128ea0da7',\n    92000: '72ac917c4f531f91f65fbe33e78b4d4e2bef18aab3d2ff584d59fed1ba2cf398',\n    93000: '1100df2fdb03d44530723d131dd7d687053db6a317ac9181cfdd51925152df02',\n    94000: '6d50f66efb571136501340fc00b52105e0011f0f11fd68c9b185717bd42f9307',\n    95000: 'bd0c996d9ec40d5e91c2b5f278a5c9c9ea0b839012a7d135e1afc73a725bd8d6',\n    96000: '4ebfe8a3e1a1625632896d5d2970d8b080f08c5948f0ac38e81f3fc85db5af5e',\n    97000: 'cbc03464aa3513aa33b540d0f026b93db7607c4bc245af8fd88c3b476ea5e394',\n    98000: 'c9a185ffa55d01fc73c2e60004b0c8dee60ffbac7784cd16046effd7a0a51a86',\n    99000: '1eba97d5ee69229f4cd697c934d60cf3262ebcdf4c79526cda71e07b52fa22f7',\n    100000: '0b1650207a55c64e7a6cec62ddc3cc190ed3796507bbd48358a0b7d6186bc3ce',\n    101000: '1f47841b0034d3cf1046660c2246902f679445b5ef621df2762bbea33b7d685a',\n    102000: '42f63b1b622d08e120b5405ba65d25e1a9777312cca77248b7827d7084e6d482',\n    103000: 'ef4861fbd4e578ff80e0d5e2afc62fb113aa203d3933f74efd3e2400d73de922',\n    104000: 'e97a3cb78c09eef732757e81c6a7135beb33d95398054b45df738853acedaaad',\n    105000: 'd12e8feec15893ccab662a1ad0754b2ccbc18b078ff3894eb2214ee4ac2f7af9',\n    106000: '83ae84103b37c93b2d1535fc47b053194270b3c186b1b25d69cd8a1540caaa1a',\n    107000: 'a102c6dce88bc4695250c24a0ebd2913b2b85c211b7d5dbbd18ecd95bea63144',\n    108000: 'a0c20b36140d55288dc4dfeb6c16a6ece3d43efb57b1728bc872c35d6660c704',\n    109000: '412fe428b2929c3cf5c6991a2657e5414a569b30e77a3f4fecef14239d89caca',\n    110000: '8518706e957f3ef892d1d2d7f65c5f4588620be994cda9cb7d81fb600554a456',\n    111000: '66f4e25f40d24360299e5358f954d8a79b245399c46dbc4e8a3db0c10fa14e18',\n    112000: '81c0ded5c5e30f92eec84ca293abb0165068d917bdcf436754a0e9b0ef1e325b',\n    113000: 'ebd5e034a45517e59e96e986b55e38051a5d2a3120a7ed92d4f3e01e18e73972',\n    114000: '681b85720d71ab660c7443392883ed4bdcf9aceffe202cc5ead94664e315a744',\n    115000: '3fbda839115bc6e6d451a05f78b27b35b137c94374fdaa42235f3035980e85e5',\n    116000: '7bfb88d39fe0ee7ce046648676b226997a812af1f1ed79040750045536c76067',\n    117000: '5853c6d48fbf99b06d3e1f474e1b667ca598712f9feaff8cda48a95935cfc498',\n    118000: '6a23987731379799289d2a527dd40fde8aaca8398625d2a2e3e646372ffc99e1',\n    119000: 'af73cbe957865b60c3c3139dd1e2bf8bf1cc504dd167545463105996609750c4',\n    120000: 'b309a20170421fc4ddfedfd7611716e1cf5a802a1db09b66b9ae448fdf958792',\n    121000: '624cc893801e4da093ca973e2135352bb428dc278e47f5308412d05acbbdeacc',\n    122000: '2f20c67ac3485b6e5407762a23e8a7df1bb380f1607f538cb5e2e1ffaad578ef',\n    123000: '89c73ed65ab8f729cdcd678dc52e876969907f1560898220702a696893744e00',\n    124000: '17d8155d97fa4e8705aa55969709f9cbc97f87815d40f5a2b5cf4d7b85dfe4b3',\n    125000: 'd3cbbdb7f51c8a1f5794bb3d1f40c413b313f91bd3f3dec0d8ebb6a02107e52a',\n    126000: 'bfd02d760eb5de2e792ea74215ae37d13174c3fc0aff08587281f0f39e427caf',\n    127000: '937dcc09a14d1de79f5eb679996225995cdce69497cff5f12c39c098c42656a0',\n    128000: '81d372b07e2467bfa62bb4b974d0eb75ea313e547b9d4e62f3c617f939bcf67d',\n    129000: '9b4b315738ab8b5266b5accada140dcf487b2f509f1d758b9b972b93ecd03c9e',\n    130000: '8ab3b1862cacf7f1c4f6617f7280ba31547dadd62bec9c37ecc8d074c90be2b0',\n    131000: '868d6520e526b8594c44a05eba67ec1de8e01b547b34b62ebc5dd09d656e9072',\n    132000: 'ce09471822965da31db3188cd156538c44337da2952486a34dd2ab372ea490a8',\n    133000: 'b0c34cd700630c9281c8657dd24baf609f72f897442538d77f4ac355199731c0',\n    134000: 'a76e5dc895406a3951d55b557272915810420bdc3ac076863c7efdef3b115890',\n    135000: '2b70fd7021d40f6df15f3f72bc8e034a8a0bcd8542847b5b7a2088253351b3a4',\n    136000: '6161ee84f6fe100a02de0675bc9500dc18ccaec74e7da4b92135f0ccee6fb663',\n    137000: 'ae5770a7158089ca9adab8bdd07f1259e6f204ed377af27fb6a772fe1c031864',\n    138000: 'f3b8de21370968a90cedfcb1cb6803ec9a6b7740a94d8e4c60a80407fd55a12e',\n    139000: 'f08f6431d7367b71b25b321809ebf48f797bf7fcc943ba366420fd3f3dc00e5b',\n    140000: 'e76345dbb8c4c5ed1ea37249f892f64098557700259fb360401a559c45909041',\n    141000: '776b333f5b221f6b443d6011bc8d0753eed5ce2446a6d3ee99a5800c7159fa92',\n    142000: '9453693cb846f27ba2ac39194e5b70cedfaf435b73b5f7f288dd81343ff605ea',\n    143000: '08aea64cc4eb0170d2704cbd3a4ee871d4f3e53a8c1395288c029eaddd1aa097',\n    144000: '801d60c225301481c761114d5152780d5002b77aed18845c32c9e3804a5db254',\n    145000: '78cc80d54933b84248aee32a18144d03237f948de7889c97e2112d17ac998009',\n    146000: '1e954e8f2fe59da3feef0514f6b271f52961467782b2530501c294a83181f176',\n    147000: 'c0ac42fe9c0e1d2b0b7e6e796c553eafbd5d7bbce7df84903dabe16dc56ba863',\n    148000: '75b5441fc1785dcba69b9409f24224ba3207d624bb0e7d54af24118d08c788d7',\n    149000: '20e53048515cc34507cf2d1dd160eabafaa97793d81626e9fce38d540142dbc4',\n    150000: '5ff7a08461fc3fbacf8c6261dbd34a790e271bbc39908c23d5417f0f7a71b71c',\n    151000: '5c14cab7670821966d5aa61f09b32d93c865764b6e258764a3c33a82a3133fe7',\n    152000: 'e595968e040b54ed6572862b4089e005238399014d99bf4c361cc5922dbfa6c0',\n    153000: '1292bac3fab11de09f46a8327511b7c37366d0f2a96a027f4bf2f16147a39af3',\n    154000: '3db31267722b500dd9b26f5cfc4f06f26c5c343d181464499d3bb4aaf13c3c20',\n    155000: 'b92962af73b42335125f28d7d3fb97e3aa6ccf47c3f4229eb7fa04b67fdb933e',\n    156000: 'fdbcf12415284d8cc0adbf16996b9d37a943fd1a924c5dee5f47dc8add23f278',\n    157000: '199781ca8e5929708f834d6f38ce3e2fe196ab519f9915f14401462273431e19',\n    158000: '969017fd15cf6a73683de287c5e19fbd69ebb5aaf7c13339297f7177a716de3d',\n    159000: '77c7995cf97218655c195fdac9010c599859a46f760d84750189114d2d2d1d9d',\n    160000: '00ff84f0d845b4f1099d970cc0c3e2dd1c2584c611449f2318a3f27327246d51',\n    161000: '08481dc5f61e776ae8be12236d595549abfa0be28b187d80dad573594d94c11f',\n    162000: '71497ab26b05453f3c7057f8bf57fcc8ba30920a6032ab0ae3abdd29e0677582',\n    163000: '08713e0b750233a3843241d24573c4800f32c894000b7815860048ef3e7a06db',\n    164000: '01fd806f1879285ad5234f38789074b0ea3a0b2707bc6b49aa9bd51ecd26ddcb',\n    165000: 'a990225a2f02a77c1c61d518d46dde1e1cff3f4df0ed24eb463b97f84c7a2616',\n    166000: '7772a601a6e765ee340eb666cd645503c9988430ba4f558961246e9e69349b25',\n    167000: '389c7a8979078b57d54936497680c7908b9f989e46631358f1c31a3094621557',\n    168000: '2e1df97fe3cba7add5f05734a119464ff7c22bf92701ed85642da856ec653aff',\n    169000: 'fe0adfe455f65cf90e1632ef39bf9ca5856020d995cb71ce8a4998a40eed5998',\n    170000: '87487f255873e9f6411565553fa9ccb9518e7baf71ade67492536278f1ba0feb',\n    171000: '37cf239bd630b7c891019adcefead4baf19a86b40e21f3dfe4a76a78f2985103',\n    172000: 'e2f4087b868558af51dc8f40d77c300d76eacab17e5fb8bbf3b1f99c02d995a7',\n    173000: '273f32717f740438e93063d259dd4f0f160077c34c937a7d6a9b1f9fbb34d87d',\n    174000: '807a861813ca690a6fc0715f354f36bf491c34588b5ab778c0f4e692ec3fc152',\n    175000: 'dd74cf3d686c59820885bab134c81b7079f2edca86a25944fe0aa36a96941550',\n    176000: 'aba8163b91a905aab5ad6e7599b919fba650c446938dc7b2352759203bea8957',\n    177000: '260bd0d053038a54565f9b25c3894cfa875c9426b9d6e9915ec1df03bcf90ac5',\n    178000: '7a527f71743c4dff25f13b918ad4a6d91fb0bf9c873a7282e51cb4edf545edbb',\n    179000: '945397818b80995ac4e23c785b6d5ed2eb01338a2d4ca6f0cf40986e87659d86',\n    180000: 'ba1b5263f5418844796a19db06a53c095ec0428696d8ee953790d6c86de0795a',\n    181000: '2b673859af43554accd8599bca53c2ca94500ecd65df7672a1e586595bdec7ff',\n    182000: '08bae9f096e348c8c6b42ac2762298ec3faeba896524968478fff1d40017b5b0',\n    183000: '211577ae9a4f93fa5dc3e33764c31048ee306b5e5ffa5081fabdf3f30e49a977',\n    184000: 'a4301eead637043a8751a30c13935af971a731e03e90317c5a16c1677e50d837',\n    185000: '1f4aa362281293e8328b6c5b32a9948ef03dcea81c6ba13f94531f0c5e42627a',\n    186000: '0c6b15f2164a1b55bdf0eee2b0f5ab7cb9d2dfea6fcc82686483aa7a3659f6a4',\n    187000: '5bfa261cad0e4d271814de1f4752352f35da10f24b8016ac0065172e1296052a',\n    188000: '94c2490bdafe70d65951e4034e5b9122fa6b376a5ad8c9e6276289c4a5e059e0',\n    189000: 'a0119c5d57523b7ccaf1e5c6671eae9f8562bbf2477f7dd8c6847c6da41707f6',\n    190000: '4d82802c2464053c391374eb6124f8839faa9b343bae3f34e00f87fad9f45f9b',\n    191000: 'd341f5b4d20894be520eeb533d56c5737ad02a7ab0c24b2463516ff04e3b8b0d',\n    192000: 'ddf1a72ba512786d39f7c313f710bbd452040f1eb8a0c0ee7a4b9192199fcc3c',\n    193000: '514e1b40e5e15dc72e75cabb8d490c5e76b968e6dcdb3acf518b47e3a2020407',\n    194000: 'd0bac7ca0636b662d6be169bb5af807c891523a44fceda68f08c8934d6e9e254',\n    195000: '00c37e81f0dfe63a8fdefd0ba91442019899d50f72b43c4de8017e767777a5af',\n    196000: '581a9d093458c0157464c42fa2e2d9a1f61f7edf1ccc5dd77a4fdfdd9a4b37b8',\n    197000: 'd7555fbf7671d2475e38f62418f6130ddfe85b8fd07fe8a9815849316165d401',\n    198000: 'ea388884ad5abb06ccb1d0d2202dfef5c4ce4de0f3040df089017d01f14b9530',\n    199000: '6a80cbb22620b7a5f99f4ea36380b7bcd22e87feaf4186bfba0b9aabcbcaab70',\n    200000: '7a7231293d242887c6dfa517a78b60d40b41456b6f2fe833c0217074664a61d7',\n    201000: '0d6bde8cdd0370964b83ee766891b3cda7b9eec098dfaa5ef05842b29bbd043c',\n    202000: '223fc091bddfbaedec92099f873fc1a6bc50556f3421f472a63977b3351b95c1',\n    203000: 'f87fb207fd397f8fb5c5c21f84f272cf33d840e20ccaf9de810af784d8459142',\n    204000: '6273b130d819d84492bc50fa443ca401df748e58fa84a3708a1797f116f60f1f',\n    205000: '498edfaaa9d5f0501aea5482ffdd9208b86af46fcea5a1cbc82d51d45eb6a256',\n    206000: 'c069138610493a70742918b5c624cbf9275bf55ee24649e32244238433883ea0',\n    207000: '5110744882ba11b98df95f11e100bc398a5aab4c8258133b518b5abb58c7fe64',\n    208000: 'e5690d4e0561ef015d6e2997df3b8b95163cd884d999d8c9ac212bd7e1f06ab0',\n    209000: '17a0a3bbf2fecbfa5c7667524cb2a452f49b8dd089af132f9d2c530ca0b677b1',\n    210000: '3a9489f3aca89576152949a67df6462d25bf3a78976451b5cf8bd8385a10e6ee',\n    211000: '45196e756c7c282c4a9e7133adb07f103d0f74ee90919cc8ad89ca3e5d38c6db',\n    212000: '590f21c83694c35028b9c9d632a082307b8bb27ec87e2c0c4e2881a7be9c6637',\n    213000: '46e56c45305c51e4d22927e449a1855064504cfb58cfceb0f293d8a4f1dca7cb',\n    214000: '50b3f1bb82c1b213ed2cfd2c1144b8e67e5fa731dafabfefe063f8658c5536d7',\n    215000: 'f53709db4ca6c12b80c64501eeb8f176211116b0fbc3a650082cf830792b206a',\n    216000: '6818db3f6582e71021a84fffe751fcb47db2f734d59547be47a373575b5c7d8e',\n    217000: '01f6f8a0699ac533ee7921f63334af07b04369d5e580c1c609c6fee64c8c6b8d',\n    218000: '81e9ff93cdf5686fc42663a056c74fb0f560c29a5d23f21ef92364735a865392',\n    219000: '80b32bb6bc3c54a19d3b604f56084b3bda13afd4af4130f791505e44fdad6c98',\n    220000: 'c427f24ab14449b1430c5302385f3a77f90f19715d940e8dfb7c054828270163',\n    221000: '4303a0e1c6f8489589290370a6843d97eb02a01be8c227d905cd0e30034feae6',\n    222000: 'ccfdc1768afc18cccdf2b5d90d40158d86b110d878955b6570ad9e6e40063008',\n    223000: '6baefb7061239c02472c4783de6d32936f7fa1da9fa6d2448b4d92f40b89e79d',\n    224000: 'bdc334f5d1a3bd1d19cf589e3b1ab0e8a324d44ea1c70297ff2de6a66c99399f',\n    225000: '82d29612f96c55dbe83655db68afe39146aff1418af929f05033fcbe612ebcb0',\n    226000: '179b4144f4d644f546f001273c1e5382a8de50f84ec06a2c83b02808a9167d28',\n    227000: 'a350e96895efe979223475151305eb8d1739121b566964033b9967031ee5581c',\n    228000: 'd208c66a9ccb1afde8d1540d944bfb43522015f9d2df50851e66ce1b84b06c0d',\n    229000: '95cfabe5d18dd8d8e4a57b7bf7052ab09b7138ba0b3c9d2efdb32352bfcca149',\n    230000: '3e399d08c33b8fddeb687deb276afd3ce101d19f5a71b232532951f05d46d8f6',\n    231000: '91c0c17d5df1c553bd7dd6db8feb58a4436d598ef4acb1ed9a06da65f3cca83f',\n    232000: 'eb3928a522dddde176cd4c68fa95f1e953d74f3759af4b030f7e2be4c466c45f',\n    233000: '3ac946df5a319d31716f50c752a5769471c0f3ba4b1002613996981c8f32fa3b',\n    234000: '4c9cdbce1f2a4324368130c154e2d24e885877c13a126a847059ccc289fc922c',\n    235000: '8701d98b254fd0b588f022ea140e936d496ca8ccc0890ce133523432f4c09e6a',\n    236000: '0d2d45adcdf950541c25ff5b2b87dff5faad479094b1f68804a62c3151e6d598',\n    237000: 'b8924ead720d0e7a304c45c209ef3fdb90cde437d138bfed2f1e8357fb3d951d',\n    238000: '4c9c62a3b3f6bbfb08edc23d63f9b35ed94bb38393fd1761f9b9b810bb72f68d',\n    239000: 'd01494b4999c2c4def2564eeb1207fd1f3d35a62fba2e61b386bfab498cb9b6f',\n    240000: '3021d8f0edbc0cd2ab87bad5feffac54924b51eac9f6fae8ebe289f474f11a8c',\n    241000: 'd2ecf0170093d3a2dfc876f673e565e3e4aa76d23896af51c96c004cb14ce67a',\n    242000: '2b5ef67326178702a1a35fe4a848d22b791114b59121e41a25ddc1bfac82c6bd',\n    243000: 'b2d546f127252c3fa5f7ca45c61268dbaef66027484a1bcdadd281fabf97d34f',\n    244000: '58f9b398b871dd62952899d31661c118ca4f0d1c71dd165523b889d62b154393',\n    245000: '504760f0e46e2de0a2c8b4dd3e93c19d0f3e0e1b36916f02cbff166655816206',\n    246000: '15ffb23b844e6ac648a7fd099be8e631cb51b3a2496c67ac2dbedc4f6818d7a7',\n    247000: 'e3e56e8575552202cf35fc883bfcfd92cacf8013a6536729ec19e53bdb0db128',\n    248000: '22f75d137c7fe42a80ee04a386fdea8cad56e7ce8601234b6ec676498edf9e34',\n    249000: 'f84709269c393cae8106ddb1fa3a48cf3226e97bcc108717e483493159921aa1',\n    250000: 'f3aee545330f18ec49f6f880d7f9fd31ed6633279881dd13177b0a687c51b000',\n    251000: 'c2e6a4cb3c6e88f523db2ac24fc7fff3b8a27d13ea7b5d614806ed96247025e4',\n    252000: 'efba2c1f91a13ee8d7a8658e70893ccd3014a7019c225e356c3ee67955b58756',\n    253000: '9097ecbbc1279bfa2c92822a6485714ca7ba35cd3dad9ccaff5a8d24645eae92',\n    254000: '00a41b720ec0d7a9fea3229b16ad8d065cd6bb1d14587eb3404de33e45db9454',\n    255000: '7523636406176d391928c638e15e959a9d0255aa71dc9f3bea8995cae02d1d51',\n    256000: '8d3f7c5ded24e2488e25d6030d78d351354734b8b058caf6143430e96ebd5d0c',\n    257000: '48fbbc3737a718163a501e52964eb58aaf4163aab7ceaa4f75f928b25c376ec2',\n    258000: 'e1052e7ebf83246770a740c0c6ce8f011fc7007448777c533d67fec810fe5e64',\n    259000: '043f7d98b500fafc248549c1f5a9727fb0cec4e3e5f13071eeba459fb9179d13',\n    260000: 'ed2f3960b077651ac554b277dd271e12e6cd05942fb54b9464f18432adbe5ca2',\n    261000: '234f17bf2054ad4d3efeae165c3359a14e08d2e53506c839cbed6ba3d2a48ab5',\n    262000: '4342d662c4dab581bf1c123e057e4413bbf74b7a2a331e80f7099201539be54c',\n    263000: 'a7b1380371fbfe64bfb94bed5ae256f7db59a4661e461221cae99dcd54c72a8a',\n    264000: '4f2a606cdc5f346e0855d962cbef7c2e4993b2b07b86ce658a796b1baf6179d8',\n    265000: 'cc09cde8c101eb17b5457bf976525940904cb839e70ef3335780eb0261976826',\n    266000: '3c5f24533723afb124b2bc8c2f8d373eb5640dc4ea4dc76b841e81706dd6b29d',\n    267000: '467c5ca63621be011a07129d0c3ea785e2a0853cc3c0d1e9cace122831d42507',\n    268000: '07df13a794f6c0fc77142fbd0afa23121ee2a2a23bc9abca35c164edb5f6ad1c',\n    269000: 'e2be277e83a997a111fb45948e9a7afdd7925feb1708a84c62968cf6a42da1c1',\n    270000: '463425dddeeac2f27ee717364073229c333a7ba357888623c3b15a1db52542f4',\n    271000: 'd62e0bd8584fae7e4893c18252ae8d1c245e2e4c8e2f2d73703c681c6259f19f',\n    272000: '85cabf5254b6b6292c105e68d44f78d6effeec582467caf9d30a5deeb43b6a6a',\n    273000: 'f5509cee847a757c84adea8e40f3c7c0cc6a4da33a8cd3019cf9ca850d0bc93b',\n    274000: '2cfdb3477de2fe1c81c7133094094c79e933cfe6034c158de07ffd0c4c08b564',\n    275000: '1c1b8e447e95ec3c306fc68528631682bd85e1c876d86223fea28b7ff383c16a',\n    276000: 'b37e5e4f6d29590c9a58773a4d4384cf7c51d04fede4a602ba8095e14bf65993',\n    277000: 'b92d6ffdecdca014c946c2fe8c6c404ccf60d8401e7631918da8885bd360daca',\n    278000: 'c1f82e49c8f66aa580a216c8e08262080ff9fc4f4df1c482ce8b2695b90b7240',\n    279000: '41c48f5965e8939d1df69127798b46af17646ea75e545a7b47049596d9bfb3a2',\n    280000: '48da55fb8ce259432417ffacba5e064df4df2a8293cdd484ee0ad8ce4b0bc38f',\n    281000: '0e892fd450a8ca018567e1a6e23bcdc5b4b872686f816a415991f59b3abd76f3',\n    282000: 'b90fb78edf920d0bfb4bce1f39327c6777b511fd05d715729d7c4cdbf449d59e',\n    283000: '80965fd3817b9509ddbe6a919aa73adb11af64cda5d8352ad2bd9b7824e95c42',\n    284000: '43447043aea93b162d6f615e8cb7d992d02c176a41b0518256f7e0d8fb3162b1',\n    285000: '33bc92c77ba88251822698a7bd02860ef9c12420e4c27f4020370acaab0a2f37',\n    286000: 'eb664123736754c19a47e2afbbc8645c598d7b70c3d42132c334cfeea8fd6bf3',\n    287000: '6af8f2fb3ea1f0fd0d6432599c3a681be7d384cdc553903bcaac8de2d472725f',\n    288000: 'b5fcfef1b5f6373bfc9322164160fe98fc7fb4f3117bf91c8fe61324db62723a',\n    289000: 'f6f1c6f8a2a2905b2ca18845b8bc771bad6e9cc32fd2c8e20a565cbd7781bac9',\n    290000: 'cded625fb7b923d7471ee28276b6983bb4fb4f6281ded9a8637b6974710feab5',\n    291000: '49af69a4855a36d00cfe2408f7d090fb5001913aa06615eca5af78ecb92b1fb9',\n    292000: '2dfe91f7f1870539e6b51ce58f9cd6415ea8fd50dad376d74941a9ff1bf0f8f4',\n    293000: 'b84f14b784cf6eca78d13f4b0db4714be5bcfa59b73974b7106a0206a8993767',\n    294000: '07e95fc3c141dc64ae192cd233f97ac16b1adc9dfbbd685475bf693930b6f604',\n    295000: 'c39cca37abe0b8839f8e797287385c73031496c632a064a89b9dc0730a3fffd2',\n    296000: 'e69afa519ac761328358f5b3ac8b4395d5aa4b955bf1b4c5f3bc0af49cbc3756',\n    297000: '20c93e2d1d9bce1c861eb16febe50e534acc3e2d142cd2d0767bfe076545d1f8',\n    298000: '75970b654ac884cbd1328b68b59963bfa5babacca4c7d48368a1d938feb8af6c',\n    299000: '05448c7f1230870e814858ac9a7858fc72a045dd9e0f1f14b8862d2fdd524b07',\n    300000: 'e254223f85c3ba65a0295a5538387c08ae94e6070f5c989d27349978fef077c7',\n    301000: '4a8bd2357b4ce05422d51e052018fb9f75ce3c07c53b5d1623ac19afff388e63',\n    302000: '18f4a960b810034f5ea02458bbcb9bf3d38d681555be4e40d132f867444306c8',\n    303000: '667cd927ba1e7ab162c17a711bb0ba65f838e543f46b093c86c14e39988da156',\n    304000: '55280e92a3c3b9ab9e1032490aff10c1a2fe9e96cf4f88e49b7a756c0491701e',\n    305000: '8af5b503f1819b23da7b3663314a15b10e39e072377d1b08518a373c85515c1a',\n    306000: '439796b2b4add7dd0fbe14bb3d04f3917e92de2afa07da2a384b389ad9228888',\n    307000: '07fd4cbacddb504e23880dc454fb70659386afa21dab3e862cbe7688218a43fb',\n    308000: '2734a4205be5f2c8ce5850bee8d4ad0829c208911bf4ce833349a43af2db8fe1',\n    309000: '261b84349406a913a4cb700fd218112920910c1c57922e289114c7b903fe2d9f',\n    310000: 'bc652e239edd3476df7bd1e0563595145fd4f2b37dc2ac3bc28ec089a7550812',\n    311000: 'fd7c1b32015a2418efc6e1a73470ea3205de05662c767b0d4198fcaf804c3b5a',\n    312000: '4f017d27b81da7eaf62760cb7fe6613a6ec28fdc18859b6e727b216240a86315',\n    313000: '2d09525e2364e7cd7fe45191fee11f1ce16a2fbd16b56e41f90a3c64ce2f944e',\n    314000: 'c05770490a47280c5b318709c264b2ab951a5bf8c4575cbf1b1e3dbf35323f45',\n    315000: 'ac5da2f5abcee0f92f29479c6355ddafd1f3b4b3cca6659117c17c9fc10fc743',\n    316000: '4496d1a0e4c22a2521595bd4fae60abdb9404f79135311606a9e6713be373271',\n    317000: '40ba7c87490677af9e8b033a19ee5ffe51ba192f5912f491738bb6485cfb76d4',\n    318000: 'f42045295ce30bd4ba9fd9c845bc1228949b4152517185d3dd0598d8bae38481',\n    319000: 'f488695e46b53e4734f4eb9fced93f067f037af7099d0f9510856a898a0c438d',\n    320000: '83e42370911c70bfa2ad89a82ecd02bbc9cb13d24c2a900a0030771898ed618b',\n    321000: '74eabfbb64eb89488b59393ed529b4ee97e0f88a7fc14f798f22715bcee3fd19',\n    322000: '54f42438cf96fe8efa0180a47350002b34ccfe82123a85a9a6dc6f8b0894f0a2',\n    323000: '5509357b42476732435a1d5a6a850e27f62cb3ca81219a94f08dded06dfcbbf8',\n    324000: '1cd477361dc4c72b4f157469b6f3616e060f5d058b44ba864225210432b35540',\n    325000: '060ae9c541eee90a80a11fe300583e47c0ad4d12093d0651310cc6c87ecf2b75',\n    326000: '13ce59868c947ecf2d6aa986501c9f8411a59b4e0b8b6113f4f45dff4074fe59',\n    327000: '16a1532ff983f5cdab6c2d465d58f0369665a926c9d877212d95f9387e8732ca',\n    328000: 'c9608a16fc43e7e94f04b3309c067b0a4f68a66705009dabb39898f41fdc862e',\n    329000: '029b02acbe0fc8610894f0b89d6bd274362195a443fb9e5e4d4485be7786d461',\n    330000: '01181b9529da87eb39510619285c08b05cfa2ca2c7a2fb3ecadba474c1db3aa5',\n    331000: '2371752c35da119184e53dcb593a193fb1c81d6edcff921599328a6142a57e3d',\n    332000: 'fc025a8f319725faa9903eca86935116a2aad8fec6193055d5d5f0f431ba84b2',\n    333000: 'b602651afd9988075887a3f48455d977357032185b7d482262efff9cc97a45a6',\n    334000: 'a9a2bfb6b0f8db298b5ba732f4a23242fa89e1c5038916ccfba3751f7dd1b4a0',\n    335000: 'eeb3e35b6ea94119bf058e6b02a2ec2a706089bc1c360ca28d1b4e2ce1b547ac',\n    336000: 'e834f7ac6bd8e5b46ecebda4997ae73bccf353fa024fbaba7ad4d1d70d08d14b',\n    337000: '229a7017b6d0ffa08374e297b07f12620bcdbaaf5977dd39f3f207ecb03c7adc',\n    338000: '83d86500e8b2ef46a0f1505e29f7401b123ea5ed5feacb49864ab1de284eb574',\n    339000: 'b08e9dcef3e38d631414b4d3a8d8b3dfa8a07d9e5829c515e7db2fec64c60827',\n    340000: 'aa36ce6494df6905f4516530ddca77d88341c0202bf052347a86e090c03ab0ad',\n    341000: '527b50125d5479117ca8bfe84fd5507b2312360908d4cafe8588d40c4ef5622d',\n    342000: 'd1397e8b0f847b6d46c409659e9e10f3182ad9bdbca563cc2598af37cdc080bf',\n    343000: '896576aef0e0dbdb2df8291f2832716029f69718ac79f355425d475a1b2b7b4a',\n    344000: 'cf4b3c75279b0f7d71cbba934228709ff03e3254fdd83a47fde66e83ff393eb3',\n    345000: 'e68c760edf370e758e815d9363ada456fa2568b06abf8626f28a32cc62944770',\n    346000: 'ab19fe52e780e45a716ed97e93084bb3f7916e0a4479f131c7f5f7a2e7d69d9c',\n    347000: 'bc77d338a10fd71daa6c68fa9df2fdccf44b6d9b1d7c6456bb281bf8547b622b',\n    348000: 'f84469ebb26540cb3de0ae03f1f47452f9a9bc6b7195c3b5260d48623507ed86',\n    349000: '5bafc3e6dbe4bf88257c4381cb4f2fea0a4cde1f272e1f1006600b507cb0f670',\n    350000: 'e53ff96761b9c98bed635457acd1856a2ffe65b84e339cea94ad30f13a31e5e9',\n    351000: '5e0eb50d32e90e5ca73639313e5d3a850a113fbe60ac494ca6cdb799e9604d7e',\n    352000: '1a1a04aaef110493bf8fbdb8d1784a7706c6e303773ad0bad6511dd3c1da6c54',\n    353000: 'd72f3b615e244584f131f2f8e7154534aa9d2d9c4a710bb4de44de556b5d1232',\n    354000: 'e81b823c64229c555a50e90a4c3c4654b6673cbff4c62c6e897d09aac00d95f8',\n    355000: '11eed14cac2024588b389ddcd54315923ffff03d6c4c5f32657ba551fd308516',\n    356000: '465f28c63e564296f75a4a22c3ad16714cda29d7a2797e51e512eb1ed5f6a49e',\n    357000: '2b544c79c6f74ae1c05c268669e57ad41d6807f1d0ce1a5024b7b08646df6861',\n    358000: '8a76eccf113819aad8d7c17a459a614274bfdaeacfb4659d945f13dbf3907ccc',\n    359000: 'e63743f458f1479593d73952bfb882c72e5889331add576a716761eff2cf13dd',\n    360000: '0ab0bef439debf08dc3e1df9010c4e119d6862de6620de5f725cac0013c06301',\n    361000: '50f649f0c8bc0d77ffddd53fd5b896705a8858d3684a3ec876cb320ad26d2c29',\n    362000: 'e058c43c366c9c307ca3ce5a732f0ab2b4e09ff291a067366af25a82fb3cddf1',\n    363000: 'f99d7f0ec93ec7fd74273bb6f8bc4ad8ec0fcda9f6406001668dcf62a088a867',\n    364000: 'e5a69ea8763650542b1f90d6217af00da137a9bb72cd0dbd5379570d1fa696f0',\n    365000: '5d0eb3063474c6bbf9d9581898fcf8c4cd097fe5c6e3875d7cd701a5a0302507',\n    366000: '14da2978f4d4eeff5eb0a6bb663e514095acfe2210039df8dda9b76f522f74ba',\n    367000: '4ad3ebe23fe9b0f407c0765adb69ee973de5b84db29c97167c7a358fe8696bfa',\n    368000: '22db57ccf8c8193ae9fc08abe11f1ea8e869bfd6ab2c0b918820acdaafc19136',\n    369000: '530052d61878d20282a69e0bace41857f7edea6d3e9e92fd0a7781c8b60d208e',\n    370000: 'cd057d0c889c78e06d0eb71287c46deea4340b8c7a4c470dd69020e1a9029c43',\n    371000: '41f32ab47863f0c44b8755ecf9b201aaa92999051ccbf8a5acdee0e12dee4110',\n    372000: '37de29bd97d75a41dc3eb52bed9c09baf069c23120a393888e15e61ac7f81ea2',\n    373000: 'd6b26bb791fdf6d79f59d5c37ef2af0a35149737029157724224d614477527f4',\n    374000: 'de18c6a921838385950b10ed9429579e95a0bdc574f448f457ee486a59a04260',\n    375000: '1f86edf4ae47f409f8de6a16c17c2da063937478a0ccc800633e4d33b531709f',\n    376000: 'a0511050ef086052fe5671d3333c255507b03edbd365cd7ce7cecddb4000f391',\n    377000: '7b8f0060f51fd170296b9e000bb2970097f77ebf0b5872a9ac01be5aa03d7ffa',\n    378000: 'fc5669d194d822c55e141ddc9aa229059e4663169456dc927d43b44a2c8d3c0d',\n    379000: '1c0bf45f052e0123d1a6806fc4a93ac8261ed26772f324c1b3e7c3fa44d7f442',\n    380000: '73b69dcfa0a916a4c495a14d4d43e270b123ba37d267e3eb0d65890336fd3e32',\n    381000: 'e89e3d548193df667c637f774c76f508ec79b7bf1f2ac5d9adf7468006e34454',\n    382000: '6b835973c2efcd564fbd58d96226cb984d5c32dbf63d5b6044a0a11f77c2ccc7',\n    383000: '50b7011656c5903941303c7a19e4ea0fc6bc10456ba85638b0919658556e47cb',\n    384000: '74c509b238c1418ae527d370e8fe922f7639c88ef8d78fc3a45894deeb2252a2',\n    385000: '6f9f6147b06d8fea8c440cdebb3869b7bface7cb2355d66639060056adc9bcc2',\n    386000: '5b59e68fd62751b8be184a5621f1c63f1e9cbd22051487999816df392dca7837',\n    387000: '86d8b9628b83ac63637c86c364001067ee1c3050f395758fd3f0bcab20cef703',\n    388000: '5e570f7b69738285848bd8b7f2fbb82d05e99a72cd4a0faf20a9e3491d2d041c',\n    389000: '78d31bda70842388c1cfb2b83e9e95ded37a0304aaed3292e2687a96c07be9b4',\n    390000: '316da5e2ce79bbc4f4500ea6417271f0ae268f85f86f81223d0af38d1e0e0a67',\n    391000: '45cebf8a25c25aaaac06e915fb3fcc99a84be61a266c2bd8997bd1fe00a7321c',\n    392000: 'c5ff6e24f524bfb1f629245345052c0a0df33ab881ee9d11dcaf64915b2acad3',\n    393000: 'a54b5d4824bae06f91566b73c99a07b812095d7c133d1042ded80211c0402b3f',\n    394000: '0ae76082f8347ddf838ce0ad486f5decf6223e8b8cfc16dfe178b15830807c60',\n    395000: 'dc436c97ee6ed427687e6d48e609d271b125495afdebbcf611c0fd5102ec900e',\n    396000: 'a87871c13ca3119f309a4ed98e4224d4eff01387ea67e86148007ac0d759f2b9',\n    397000: '58c143b302132ad4b38718ebe1c5fa81c1177a30045f1e4a52dfa749e84dde1d',\n    398000: '33089400401a190c9375abb63c6ef489123f7369e5a35766b59e69edaf4e3b93',\n    399000: '656305fae087b86758bc7a01e13051b1aaf4a5c53d9b71e56614217a82b20eea',\n    400000: '7ea274642eac11d02aef589428b181e1c8aed913e248b357917604b00b23c752',\n    401000: '1969a9f64e316e0b4fc864fd7162379821510a944652b8dea747e425c6222807',\n    402000: '3700e1703e7803a4577de7e364a34c3be94d3f933e05c5dfbf6ff68349b6fa31',\n    403000: '8b622599bf4b2d82bc11c897f35536036e6798ac101a6256e14477624147efe2',\n    404000: '5f6f6dcea393ddccdec69b30881ab08e01e1a7e6a50d681883c759b742c12d6e',\n    405000: 'c3fb0507a57da69042e246dc8ecac101f33cf2b1fb53fe2f507ce28a0d82e4b5',\n    406000: '4f034663bb7b84439fa256f7b4ee2a5cf93e0048c1eb476aa2152453ee7be544',\n    407000: 'ff99ff38df6f928242571fed5b03fe0c3cb481b510d3c943e47e26f26440cbda',\n    408000: '89865af5e99e64cef61dd931061bbca7fe355218668f487f06474e46ebf892d8',\n    409000: '1b1d050c0b98d55d9ecb0d1a5b03e75caa352b4e0ce6a57136551000e150c480',\n    410000: 'fe52790595db35c7685e5261de78d139c830af0a5ae73a7e33da21be9ffcc9e6',\n    411000: '1afe55e66e6e16abff282c9d6bcfbc6a3a6e40db3c60323cd44e3b3a5d4ba924',\n    412000: 'b59a4057d73e8037f3e0e9b92310def7900c19917892ebe7689d5f8d77912388',\n    413000: '603f3ca16a6a795d5aa807eac5d6ee7f80a1e2596cd91cf41cf90ee309b2871a',\n    414000: '10e255807a43bf5b95f9af648f1724de1e953a5540819822be5de184ead24ae0',\n    415000: '9faf6b1c33006e3da3ae2c7599c7fdeb5078a8427edd6666290e32aea67f5a23',\n    416000: '973b77e4d491ce0c163ca64f5419b05822eebc2f5d85810f51370f56e0d812b6',\n    417000: 'bf99710d77fe84ae3ef5a371887cacc2ca1fbc5549b04ef9f61cb9d911b06a9f',\n    418000: 'f032acfc2d629fecfa180067b4531f9d72924c5f204cfe9053c6556a334373e8',\n    419000: 'bc9f085612af6a21411fcf51259387d97e6ae1169e4c6f92766a168302a98bac',\n    420000: '9136bd1fa7e8850c52e3cda39a96f754cdc605a69ea49b7d6d959d0cce81efba',\n    421000: 'ec2a78bad1b4b7325d574122ad8c3072d27fe6854a6965e6917a28e0c868a229',\n    422000: '536f4cc5d6ab082d32ccdba1caccb5d4f3145b60ee8538b1e18c6231dcfa23df',\n    423000: 'ac7ad1f97da0e55661bb25d53c27ea72f705d54050f020aad9fd5856a27117cd',\n    424000: 'f840eed1c0fdfba43965bfc175e4d92a5ddd0fd6022dfef988d70a5e7f363116',\n    425000: 'c1a8f9b79a34b76c9ae58e005501dbb1893cfbda8bb25b0d54854657b3ff1a54',\n    426000: 'e8895d8f797026a2f19ade2b548bd2cb655e2d756dcc0a50850fd37148d24ae5',\n    427000: '0ca56cf4fd882583854fca00e339534346d53178c6679f8eb1e7a5dbb7b9e7b0',\n    428000: 'd3ce901843f952df4b63e755a0f58924ae11324aad47afece9a93502dcae7c13',\n    429000: '774cf2d1ba62f20f5e1ce50692f65f0ce35c7456d3988cfbb2e1b753e0ab0f51',\n    430000: 'a07fc310f6201c30f1fa70a082348942c65743d51e3c19492596f6aab4bc9bb5',\n    431000: '7bc7f405bdfc91dd01877ccfd6142c973d81dab12fa188f468abe6c9a30fe0d8',\n    432000: '7fe40e145078c1952f87547156e2147144449971f108164d27bcfb93f1c840d7',\n    433000: 'dbe52d2772d6c0461e53357b71a79096140039ef643876ec2d27e1bdfee7e81f',\n    434000: '94b379e842d4979bbec171bb08881227ac4e9477186ab63f7eee9b4261b92365',\n    435000: 'bc9cd265de563cfe80090ce12085539bc67d406c23e4d41751f191d6b6c63168',\n    436000: '3ff03098d2c20b5cc080363a84d8295510db937272801454d429c4d17a1dbe10',\n    437000: '5b8ebd40170e217eb66990af3574b80f4e66bedd092e44836aa5101ded50c13d',\n    438000: 'f725813181d50c0b20552635ecc578685ab0218619d53ed23493c46fb060ecec',\n    439000: 'cc7cb296ae79850ba2f6f7532c445189452831957f62cb823bc83aa6b51daee9',\n    440000: '757b816392227c9d1a14f7350cab6e77a46e465e7853b1291abdaebcee2bba94',\n    441000: '834515861eb72aa472843a6924fa9d6350630ed56e35181d9fe66e3a46b6952a',\n    442000: 'cac31ac937ebb6f0cf4f3c33f19d66a05bdc11105a98ef12c32b494e19e772a0',\n    443000: '552b03680b5495c7bd6ec58db0b6767e6bde5c5961a02b7b2130ea6116bddbad',\n    444000: '2bea6ef419d9fe81de8bf5cfe21b2603f202e2b9a959d2cfbdcbccbb7a7406c8',\n    445000: 'a4106f2115b356eca92e5e3f45b8ae0e13b82af94bae391942200d1c59d34d17',\n    446000: 'ba49924afd8a40260e546e5e4633e945877ac9d5754c277b77bb154c0c9d0e40',\n    447000: '83ea56135c2b052df0fb09c68cb7a085d876854f34338abaf3cf51760c83c3eb',\n    448000: '806d77789673fa09d24c188ee024e71425df7baf623fda073076f3a9b87ddbce',\n    449000: 'c41d60b93fca1d2adbbfcfa99475a8eda152630d815eb1f6c58cbd15a0c2eb65',\n    450000: '732d315feeb485fcc5055916df17827d80e7f831c43df81ea79123882c4fb356',\n    451000: '202156d29e57c51941cdb7256918128589f5b8dbae5762bbbeacff6eefbbfc71',\n    452000: '0194e8482252f2492e1e2696fa5761f50875d0188310a0cf1d967edffb1b79f5',\n    453000: '9e63570821d8b5cd7de0c5f9fda67300f531d87cd8f778fdc6bee9519b77e01e',\n    454000: '632353fe048e83ebef942d64d550721d29b3fc8083a247b2443f2aa2b706c6fb',\n    455000: '65863feebdbd60d16330000cbae496667a67dd5fa8b8e3b6dd3732843f6222a5',\n    456000: 'bbc554cb3c0281d6853ff2f6c25b1251eb108c09e5a6c4a69a56e8f79c9206e4',\n    457000: 'fe6fdabb84ad1ffa807fd55db29dc95dafeb0e9a5eeb5ce75be8a4d16f9f116c',\n    458000: '949bd40d861e4b297352722b6f01efe9574711fe64b10c2e8dd769ad99cf9655',\n    459000: '9c139c9ccb41fbb3ecee878cf47bb37e28c76816c257f99dd7b23ec1f3eb3ab8',\n    460000: '61c3126d504fb38ea21948d2837368441389155e9675c7f1629eefcd03198f8a',\n    461000: '28ff145fe2abc7cacfb1613d40b45e13580c643ad68c44db81d15125324f4f33',\n    462000: 'e6b532924d07f1ff3976a53397327ccb232d3b2ac6ab090874cfa6ae47b8b315',\n    463000: '15ca651db9b159978ba6b2adfae1669e3042f4311ed683c72838bf337d71274d',\n    464000: '6903539d4f38f728e075441efa96b8013295ac166db50ddd4103fb2bc9937f0f',\n    465000: '6b6c2640411d900c13f5b53a9a993d10fbbd7f03f6fd477e120ba06a7cd67778',\n    466000: 'a05d282fcb9c091a0a96c2ddb3728c1e98eb767fd5311bcc08c9c10b5a0eb24f',\n    467000: 'afb81fdfd562f2cd2d53434e6458c38462f85b65958ecade001bcf2b49f9b28a',\n    468000: '97374586191b3a30654483722f7c6486ef9d9f71aaca44b72146c11d4f4b80c7',\n    469000: '90a829ff58be9ad30024ba1851515cd96d567be1d99154cb966067316d1902af',\n    470000: 'e9fbf2d2291e1b5af5dded3c223611d9f2e31fa6e1d8c075741aa155538bbddc',\n    471000: '3b4513a6a6c05ae9d2ab880a2d1aed3b189e0fca9cfa2bbc7c31ebcbffaed9a4',\n    472000: '32a57820226f672e695823620fccd42a6fe624dd2d88f7e7c76e0e9128883b27',\n    473000: '15cb7e920799939eb01d06b1b713cba3ed31ef92ca037ee98eb8101b6331e5a6',\n    474000: 'cc23e76e68dd726f31e592a4ea5d6aa3cacd827d068317a0a476f16ce468d9f9',\n    475000: '9e7e33a8ee53a4bd94a26b35bb43054b42a238a85a859ed9e4a33db978a128ff',\n    476000: '23c85e4d8edc7939fe0e5a47fa250f0921b366711c34910c394af00a4efd90c6',\n    477000: 'fd9d489fba4175c1fb3b8a0f9bb947287e64da4bee9f8a7fe73411632c820368',\n    478000: 'e287b69696013e70b276843e83af6cb5cd076000cb280e43de29cad7f706dec9',\n    479000: 'e86c7a06d96887e404c052b0fcedf10d40423b02d6b9fa7ccb9827551e5d089f',\n    480000: '74e7ad7d78dc585a5cd1054c0288aefcec0e031cd40f0ebaa0fefc7a325f212a',\n    481000: 'a01f98758cd3e561d02bef30c054ef70405ebdce9e670a7d6165efe3a9030886',\n    482000: 'cc9e22f04e4e2e4f52b8382281092ba50ec99722c9172a0e1c9ad654d26bd4d9',\n    483000: 'c92e278d54111ab2f3906eafab87fdb932307e60836df209856a97cb9e1ae3a8',\n    484000: '18fc89d07631aa01891aa44231dae6cfa43fd4733c193468b033532a4574eb80',\n    485000: 'ab546bb694363e07469dec48b559479d34d724a43865fa8f3a178ddc7b83e8a2',\n    486000: '59926fddc89abcfb55775a58e5e6e453e388f530f720dc61001f1ac59b59cd43',\n    487000: 'b41bcbd91984a3dfe72c87bb7c2ef32ba3eb3cf4318f5d37654459143dceca1b',\n    488000: '94e859b8f01ccbe9fc5bea5c274f252e23599cd444d5eb0962ece97110b86eda',\n    489000: 'fd99b951714162e55d8944e8c3ce9b098c4b74ea24017e4b204f95359202df08',\n    490000: '80b84d18f4bc6d624d4190db94920976926bbe48f2e3e0f4c08106b9f99ac5df',\n    491000: 'b0c5b14b01225b1ed8272dbe9b2def41756cda0a3fe6a1a8e75b0795b72e6a7b',\n    492000: '572dfe57507384278def462488416248a3039c1a76a8c4aa23d1ec706de1e3f6',\n    493000: '75f22a8d4d202914c0f3bffaf95e47eeb82eb00dcd2913af5dd67acf6f8ed8f0',\n    494000: '626e26524f9427582d61f340ee9181d8f238ad8acf96c1862724fab62ede8c8b',\n    495000: '69a11346ba65c79b5fd566768a8b2880a5a29e2552ee8e343bc88bcf43e9ca54',\n    496000: '314c88db22753d317c7b65f872d54d3ff672d04b3781293d59a1ea902811336e',\n    497000: '7bfcf2614b54ac3808894694e8b15940d6740760a2252ba4b47b2926bdd4bf51',\n    498000: 'cc02f90d2fd4e0f378d2ae3bf1011026ca3d7f6642dab316f5fbc2f57aa840d0',\n    499000: '841182d8519cd7b2d8fd01ddd087de892ecfafaacf9d6aa029ce1ed71ee0d538',\n    500000: 'e2742854018ecbfef71d626ab797b7c6137e84ff1e27a05b5da84b2fc7c43145',\n    501000: '07334852d0b542f897164e4d74e699678c850af4e46111d495701a312be21105',\n    502000: '6150771ac25f4185eb94ef6aa15e7f409b6beb411358ba7d982755655d431492',\n    503000: '156d3165eb11d021c46ae5968aeac20f65eb05e68d841bdc98dde156d42fb500',\n    504000: 'e9d474e59bf527d84f031ba41cf471f49e5bf0472ae1b3f48f036813a0b195e6',\n    505000: '261cda40eed5ec5b2a44edcd995b24887a45a417fec3f68507a4d600ef2abc33',\n    506000: 'ffbeca24a8f2b7cd6d64207911e153a2f0bb70d34bb4f073c33197e3a2276558',\n    507000: '14e2664bed708d57478f6fb5a4228e4c26843bd6533056fb38dbf1e7595028a9',\n    508000: '2750e25c3dde233447266f7b35f78d8bff18bbde4ae094d06e38fbf6cd2c6902',\n    509000: '5fc210a72d142dd413bc3b76d5ac772564a5917aef13890e7cee190c61b059d0',\n    510000: '7184fa5628bfe59481bb3542bb4212b535c3ede3c24425f0bf7b1b453c238c6a',\n    511000: '2b4bec04d73f0e524cf12d04bbf7b3859b489275edd047f41b139ee6220074de',\n    512000: 'ebf81767f4d26c98bce62a5f5d1a289da5bba770e88f346e2fbb1d676c155e09',\n    513000: '8bd8a99c65a258ebec809ca76e2cc91b0b4fec5dfe0970736d2fcd706aa660ff',\n    514000: '2dd7cba17366c483819ea090d3196950ee35a4bd9f28acd94575525c851234d5',\n    515000: '33ccf5f5230b9a79cd89eace91cd1c2858f6d040a11494ffb8d51ee934753d57',\n    516000: '30eafb000d199c8d9d1a55c5b09a382a3b10bcbca837e584db6647699ce6a3c0',\n    517000: 'e7ed622512e2483fb8537579ce2d2519053f9a6c8d19c42d665007632d5f74d3',\n    518000: '5180d11f98a25b8698b969c3c9a4b736522397859e7dc00665658f93c6cb8be4',\n    519000: '17ca81b3a3c942c80384c4d2c176b5274c52569b90eef686e077f53a884130c7',\n    520000: 'fe1f552778cd5250ba6556712217b86fd6f1d574143f8ad6238e1d6b67481f97',\n    521000: 'd7ac10bcb8662ab9940c4212dbf0c4b9cf9750e3c04a905efdffbce63f065de7',\n    522000: 'fa7625300aa890ccda58268eaf2daef14539c195f93311099754f955f367413f',\n    523000: 'a55ef67a0e75bea12003609359443a28b42b9037290872d6b1672433cf5c0d34',\n    524000: '08d1d556393bbddf6eda11df852a792515ab9c1d519bd91c48711a0de8faffcc',\n    525000: '76b8691b16f7dafcf980ec3e7c69c2f3caee32057ac8d61b3d3aef0fd394642c',\n    526000: '6d3ecbd7132a725e330f0a566d8d602a9cd96326a36387c1c1fe52f7f6ad975a',\n    527000: '71d08ce7aec6ff50fa1ddc37f6b1574db6e71004e8e111f04a9b715103f68e01',\n    528000: 'e9b103c11db50a121c9e3a1fb4cbbd4fb124b07a67167c5a461a539dac3f932c',\n    529000: 'd2f784907a019c1eb5830a74799562cd6d21570c4591bb038f379897f3296d75',\n    530000: '996bca5a7dfc4a8c7f9f8dfb38b1881fb97893a950f4c8f57462506782ee8ca7',\n    531000: 'c84dea2e7fb0c9c884f8dce525b8b65bb7deb33ab7fdbcf72a5382e9bac2ade5',\n    532000: '3e200d76a92dd1f6fcc420d44ebe446829fa5e0162177acd75a13c98f1477cd1',\n    533000: '37fa7cd81c658ada13d04dcf0e674017c6b7430bb0cd5a080b1b924396377b52',\n    534000: '9483c344293c376d2bd247f2846a5ff5038b27af48ea5badcde101fcd0113dfd',\n    535000: 'c9d4f9982955c07960da1e4f92803ba135f4584416e0767a3567e4d2a21e62dc',\n    536000: 'bc9abe4e4be06aa8f61b599d64923ba88d36886b638f413b12bf87ae852e4010',\n    537000: 'df8307b0097b4882740c18432ab2d7e64486cb7bb70b503923bd630ea277e9e3',\n    538000: '6e2accc5e65a85e0aad098dbc7021bba3cac37c70f6a0ff3135f86b90954bd2c',\n    539000: 'b7d9f50727cb512dbaa1ba073d227805e7adc70f9aee46b9721a65ca3d34a8fb',\n    540000: '19c695a6b8bd256fddfd00c53e6401829ae59ebdc1e71ac234f82ffb2991ed0f',\n    541000: 'cc07c3740f96ec8929c7c8714892a19815fe64f4bcdc4d1bf12cd0a9e1e82125',\n    542000: '23ffec3127ec3e093ec8221fa760510e63fcab31f9f0fb2b48727649e4308980',\n    543000: '6c73b242bb05af125ce4ea324d921ece54c9510980128605aec23cc465dbeb00',\n    544000: '51f215e5f11ae31853c4caaae1d01aac2dbafd00302b70e7198e213d485d5bfe',\n    545000: '2c36150f62c86d050e4c64d83aa0071ef4af0bf8e1e17623c740ea4ee7f3275e',\n    546000: '322e6d77f529412c4b7a1c7778253900f1976f238f96b771845b56aa080f4d23',\n    547000: '1d3413530079389231bdba0a0a7de7c85a4ca991569b2f26a1e48ad2f34a995f',\n    548000: '3576ec0d3ed469c5498229af531fdda290777fa1ab7cd83c49450f730c5ac0d6',\n    549000: 'd2a5ed3d920f273bf891ac40ece534a56f8774c8809ed7d8482ce4b78bdf016e',\n    550000: 'e1f75701ff829339e03c019eef2da1ee4239f38879173a833f81295200e96d29',\n    551000: 'fe8339fb53a33e3b5c4a39cdd64d869f41b4fc88c73fa018eb5245d2a7a01e94',\n    552000: '8c2180cc834d5c14c9801772caa65e82b44cde33da7aac586c81b2bfe77412dc',\n    553000: 'db9b4ea2cbdb21fd48a69da3bac2dbe8cea2a16d45f9252776633661e28fd1d2',\n    554000: 'd09c32f70dd94d737205922618b789204a39668b59fd3ef405ba6d4ba3bd8aea',\n    555000: '6e8efd0190d866972b469cc827330f84f515d6466d8748fc33520e8e0505bf3e',\n    556000: '8ce7cc8aa6ad30971014b332c64ce87303098e64391a9be03d1c621df851c00c',\n    557000: '9959211d82372edae69cfac47cc5c937fecbb6de9e235a5a3954c653f3b1fda6',\n    558000: '4f1d620babaa28a03d02c2ba8b7d1526ae47d45bac1220a1a794cf4a489536be',\n    559000: '3a0f1617c6056a27b1da33f0e4ca80b5b6648341551c19622f7f4b4f016ce8ce',\n    560000: '54877adf522c9abaec851a358ec6fba9957227e0e306d3a425167685380cf6ff',\n    561000: '55db8f8ad8869221ee891244767a7698d48ec61264729cae19008779dadba422',\n    562000: '4ac9c41b29253ed03c45222412c53ddde7b9da63441d06ea91b448d89dc220bb',\n    563000: 'cce11479fe1fcda6af734a03be8ea044df7e2726df67927f5bc76c33728133a2',\n    564000: 'e2e5c52e028ab1be7f5d182a217e16694ffd370930cdafa9443b627e8da256c5',\n    565000: '81488a98ee93b0cf5704cb1f46383456bf5238fdae7bcde4d0826276b5a48235',\n    566000: 'c9afdc804f7b0df600c51b9d596d6107a0cb71a4f8ed3ea764585ffc5f70644d',\n    567000: 'efd3ac4baf1c6e1fb2f8bac40f1416f189c9eb1135330ae4c25f296f012907fa',\n    568000: '225c3db3b83b78fbf9459c7e1e53ae35b01752608d329eefa99247d8a2b9b83e',\n    569000: 'ac98f3fb9084261c19a63fc8f10baeb5b03aaf9ca432d7f763a3a6706f81eefc',\n    570000: '4ee2eae15c9019466bb50d103947bdf60a5397f8fff56154ef782fd1b8579dd9',\n    571000: 'a89f29bfc76988560ccd59a61f8a323fde7c4020abafd577f88d1b0b4c289260',\n    572000: 'eb8380e096c79806443535f2f3b007378e43d2cf3ff56f3b7e6a1b2639ff3da2',\n    573000: '3182ec1555aae0df8d76dd5f37bebac6936e0e0c5609263263eef2e6e87863ef',\n    574000: 'fc45bbadf22417d49e7df89f02df358b85a4cc07adb4010d3bc0334f397b01d6',\n    575000: '6faadf2b0c3a98952bb047a58b83466a223dd323a6e32d68be5e8d064482fa84',\n    576000: '68465ee1d64b65ca71bbeeb611a8b4657d3ce765857d049536047e2ea0490044',\n    577000: 'a019fabcf35e7d4fe15e4e3a499dea2bedc61e4c9cea2803731995def15a9794',\n    578000: 'cc2fb6111fa1a338eeade31574b77a1012ec5948f88e7e5276f7ba4443df3db0',\n    579000: 'c5e7c605c03e56f221397f55bcd164dff21856f7fa48e745e61f2d05d7de5f54',\n    580000: '110fe7150988c930f9c324e00e108050b8be5ca2eaf200f54477073c72feef2a',\n    581000: '0c3adb09dca9a9924fcfe30f16bc70734ec62fd4a306ce65a38b74c5d817ee40',\n    582000: 'fc47433896c47765651d8cb31c88cbf80d0887bf071eaab9d2963af9f8bdfb56',\n    583000: 'c7c518f8634bc7c5cc536c9b77fe119cd6cdce4fafb25d4c0aba53c5bd970e35',\n    584000: '43b2357dd053608b30488aca98f5d9c151a68730fb8d008f1bd1488bfab0c391',\n    585000: 'cdfdb4ebd1c5fc3689b4f013254c832da7bfb12f3440fd327c6b065afd093dce',\n    586000: '48979b814cb6964b20ad8b322c4936c7b241c24fe6d610181c43d1cd003e5d90',\n    587000: 'c262a418368aab465facb8f1a82a392eb74e3fd56760779033a97a3f930630ff',\n    588000: 'b20dc4e0c17a223408a51b958b6b481ddeadad513e046c86c7418a8e96e35992',\n    589000: '0411c5ce49578218dc8e225c11276ed9a9802aac47d8f5837c4eadb3bd72e1dd',\n    590000: 'be8fe8b058722f4919af2834c076e2bfb602654a0343b57eb568d0ed4833fb39',\n    591000: 'affcbbd2a9fa43a64332412d649c2c0bd4b85e90e5e67576f09ee0c207a66cdc',\n    592000: '14a2d414220d240286ec6eddab0b0540ff4d1794b39f7913c00ab005e2ca2a3d',\n    593000: 'a1e2744404626b5e8e101402f50eb751c282ce692d58493ac6fa2efa43254526',\n    594000: '679da754e8f4cfd017ed5c8111361c0f9624104a87a50d077dd7bb5bb97c7ddb',\n    595000: '0c31eeb25d718f96c68de13704a0904c3a524b5936690ddab8b4b38fbabad5f5',\n    596000: 'f930b6516bb01d3365bd0e28aed56a580b4efbb45b0abed5ba941f7a56b545de',\n    597000: '56287fa7bcb53be966ba75c15b92ddf94a7bb51f4ef51037e56ccca936df4d41',\n    598000: '1a6303e68038921fe23644dd10f5cbad4af3648f7cbe8432860f439c84fb226b',\n    599000: 'add270bb4fcb8dc9c254c9130fa037dcedb8ca922c2e35ed403661a896addf5e',\n    600000: '786ea387bc7e1665655f1f683d56c67baeb9bc482bbe4401acd167793d34a508',\n    601000: 'a4c5f398f28cfcdc6e0b6fd467582ff5abfcb4f003cc59ae7703e9467cdd9ee4',\n    602000: '21b19a5a23344325581e7daf0a398b37cac5b4c335237bb8e6c14e270bdc1567',\n    603000: '000eef5b4300cfd804554e199ca6ffcfe73ec8446e82ad32abbc51f45594a368',\n    604000: 'fbf77437dfba9caa4d11b03a3673f2274dbd3d92e704bc5e8ed3f90d99d2c988',\n    605000: '0bdba251f44baa12e2b33b1ba89eb8a5d31ecaf6193b836e6356845149ea3f33',\n    606000: '2dcadcae1cdc68ab6404606937aa9981400e12ae77474462764b8077d391d2d4',\n    607000: 'eff9dfe7fb3e31536dc12c78603f023f8f1f59b2b44a59359e8bfe50b2e22299',\n    608000: '50fdd1223efaaa261f6fe592d7b9e1e32b5a50f78dfee7bc8af19250f8a82776',\n    609000: '5d88ff14c7c36602251061379179ed87c5f7d60d746d8d10fe678a0303ce6596',\n    610000: '5e56249989c623bd7f818ac1e64c5d1b1f3afec3678559d446e356ee5bb3394a',\n    611000: '5a9b15a38dd35b9ef0a028b12ab69b0993eb4bc5577d50c486fdfd032f243a14',\n    612000: '258213d8b7141418f22e303ec43005bae0508a3597fa99c4d59d656d7d768af2',\n    613000: '5ee8b46ebdec5576550540f7fe01c5a91c3ba7bfab26db605ade4e620430cc21',\n    614000: 'c41a592e2042d201bf028bbfef484d264a391db9dc9ef302708e7a39f723cf5c',\n    615000: '70fa6591c0b7bb63665f040b87750668be47058942c79c863bc00232f536d725',\n    616000: 'f4932c1c8fc84e3f4b97336424e27eeb09a65d7347097e983e1ab698a38dcc0a',\n    617000: 'd82bd9b58adf54a50bfd471c66144f2969ab443648fb7bab8299408afc021544',\n    618000: '72f7dfda9bd67b900106e632cf50204fe27cecdeea51d9b0cfde9ba6da8dcd5c',\n    619000: 'ac434b8d723ba498bee878cf589c7d61b74c7a00ea5b7abe70149ba878522332',\n    620000: '519930116d8a8f3b01ba443bbe4fe5346cebc783c632cdd5f62d305f61b6f446',\n    621000: 'fed977a0174f8cdc3e979e7435ffde2f7dbac2d4e1c63593117cfe81f320bfec',\n    622000: '1a6daef00425e6ad136d7bda36cad99baa762f6d788b92f630ed1af33995211e',\n    623000: 'b9f1e2836bd2acae3a9d21516052007005f3f67ae1fc133c1c4c336abe0dce51',\n    624000: '5d4a19f7e7f99b5c39bc844b0a90ebcbd97d88f817f8f2edafca78ae499c3167',\n    625000: 'c1afa80aebb856746da25503eb786f247ab098a94ddc58fc96ca46caee7019c3',\n    626000: '222c2ec50130a658ece226c420c76a8f7a15640033fd89ccaa29bd7c639cb94b',\n    627000: 'de9830e9cc7537c42133153cccf891dcd422203827b1377df748ec27dbfd04a9',\n    628000: '35d44ad84a3d9d548bbc8c91ca734979759e3a03e37153b650f794a0aed1b742',\n    629000: 'c119f3acbd761598673b48eaf0d223e1729b8c8aaef9a715f1bc3dde561a4568',\n    630000: 'ebfda8a47983ced56d40e95ce3be793305899cb92359ee2f9a83f7f6a59a4fe7',\n    631000: '1479c2e6217b97e98f44bbb315ca99d1ab02aad14f52d60f7e6670f5c6d0fbe4',\n    632000: '6ed7f9c90ea95825b1aae91644a68e36b6452e61f05a2821e94399823ad669e3',\n    633000: 'e00eb0bffcf784f9f8fef21b92b9cd98e44db5d790b6eab309d0b11adf910820',\n    634000: '3c71674dab2c2644d910acff4336fb02c2dd8281e30e5c97b9378d50f3d6eac0',\n    635000: 'ee3fbedd791f31de2f820cf37cf5a7bcf1fddff354c1ab5736384f41846894f5',\n    636000: '551b7598f5576c577b48ac7530e56422a5fd31a3860b37994f8489308830e9f6',\n    637000: '531b1a767734f1750077ec9d1ab57fd4430e0154deca2fdb9dc5cfcbe980726c',\n    638000: '3ae3a009b3822211b3bbe9745f006eaf3aeae365e81890205e0223925461af81',\n    639000: '2aa1de8c824fcc1a8f34269010405a0a0fb921fa0f8b998010e4076fd722948c',\n    640000: 'badc624e2ffabc438b7b2c9894e2134e36c2e80af700cebdf0980169c1cfe49a',\n    641000: '0b550e8f9b66ac3a3d0e5a911819643600dec121959fe2b5128cc6c68503e655',\n    642000: 'bfccbe9a0a127d4cf606adb5eb42f3f0ee2ebc780b719d0ec612e590aa7ec47f',\n    643000: '4d44b13390410caf86d145103471469d0b10450166f5b96b8886a440aab5b1cf',\n    644000: '2670950e311d8230e8d3447e97652cb5592248b59968bedb6a98a98c12b8d81d',\n    645000: '6703a25f54614ff79c954438ab9fae5baff32c81fa47306b337f0f87bb11f949',\n    646000: 'c93876f02c788041b400aa42149c176e624eef01bc66b49adf8fcbe255dd1833',\n    647000: '276521b16f6a98f5399a9754621fbd7999b6b94e3cff17cd88d4a1176f431353',\n    648000: '65b6238c589baec511c240cc87cb6ce758ad6cf119f465838a976a853c7f5be6',\n    649000: 'c94f6f7fa7a8afdbecc7c291eab2a3cf218ff1af5117ed1f3f0e60214ec7041d',\n    650000: 'b823008cdb0176fa0522c26b35962b4325fd526217f7eb19464cffcb3c24b4d9',\n    651000: '77024b0bfd3cc7c8d52eff6db4dcec041f8675e6f3c413b7d581bedb1b53bb11',\n    652000: '22a677eef4cfcee12756492eab7e184bda054481d4fc19cc46bc7147c097d118',\n    653000: '66779d92b9fc28fddc46ed14b79b26b3ad7672f0dbc7cf4462d5a49769a89470',\n    654000: '097eb77aede84c35822b3700673efd0129de247cb8ba5ef6b8e6dd7e85bb908b',\n    655000: '83b5b4d4aeef3782c82b9a2fd53937e4523ea15867780e4264688e860fd99593',\n    656000: '99646218016eda7976b45fbb6e891fc4bc130bfb3b5e6dcac8b11e0a4b2f59bd',\n    657000: '9ad1995af3f7f6839a11bf5fcd856f471b200911e1e5a647390a7aa26abc6825',\n    658000: 'fd5a4462737448dcfa9d81802d16f4621ea0b177f538144d836829a536cdd451',\n    659000: '7f6baf86cafbdc820437d63260fcc434694dcf0ab8000307f134fc9c50f437b6',\n    660000: '54a5f2c5a1f1534f23f54c5ac6e3f794ebb62f298dc9ed1aba4112e10cc778cb',\n    661000: '91b46c2d247a43ea916adad7edcc37e18d149fe3ab970eaf39cd060a9c856ce2',\n    662000: 'b49e0df59bfc9dcd32cd1a4b903a12accda70054fbd5f4bc480de0367f254c21',\n    663000: '29d2d3e06f744ec2225659199bd43e446308fe1e1ff16c26e0e89caa468450f1',\n    664000: '50b0665a8c7a9fd1ffb165fbb0148115f83fb7a5f6e9b8ad16c9dff9175d11c8',\n    665000: '35754b82653110c6cb0f9a0e7011ed1ff8b360301e4280e6aa2f6faa5950a369',\n    666000: 'fbc64efa658b00bbb70f5db8265e99465a07627d663c9a716ec8b42bed82338e',\n    667000: 'a046d6957f24ec9f66c73612d3eb483c65e4216fc2761260d9818486cbebe56e',\n    668000: '5d78ef63ea8ce0f66032368effcd56fa443b0f5c488ef8c243d3622113087fbe',\n    669000: '0b1b551500e2c8617150a18deec1e3b9594dd98fc9b97e20dacf2f059f966692',\n    670000: '5fa21e8edbdbb2ccccc2e3aca8e15b0965cd307ea54b6621760e654b53488d33',\n    671000: '90affcb451a786aa42ff93dd1f39961a15cc8c332613f8dc591d9f9c34d359e6',\n    672000: '1a8996b27fb79d43627caf0166f5c8ec72e1e0d09e6c0e17a0d9418d37719afc',\n    673000: '491ec5299fa141a82b438ef5d2429eb5560a0f04412878165c86f9fbb94198af',\n    674000: 'dd680d4c1101a98b6465ac85a63ef12f3ebb1eb88277d37aa341a5b387e2f1b6',\n    675000: 'af8acc98b0fd9ff347acb01801f246c63c6698aff0a0cd69b0f91e17da3742df',\n    676000: 'af24a5759888314e5ef719ee23b9f948b3962ac0b949231a540d45eedaa614f8',\n    677000: '7ec568d4e5ceff99c0e9cefa6407784aa0246845420812bcfd8a5d9c03cb01a8',\n    678000: '845df5d7e1d6f22242ffc4c976937da67ce0880a94c5bf189195d5728f208976',\n    679000: '41f895ce11bd91e09ea8b16102c8f2192cdd2c754e2cb04d33edea688fa7c3b8',\n    680000: 'cbf1e3a313abbe05d90ff1de6d861cf58da50b7f8f7ba43a593a3b6192d65f8a',\n    681000: '1ac184be2b5edfdcc0a73a6a93d3f59a5197bd5bba2ddb1ce737251526998bc9',\n    682000: '0e793523ba0a679dce1e9cf1d1189c5311e27ae8d88c35bce36ddb3c16dc34c4',\n    683000: '08e07172a6b6c3d70c4379b206ed796ea8d916d314d0b6f02539becbb90077d5',\n    684000: '0b67663fc37940b4d40ba88b8b7610776e35f070e4995c5ab09ccd4b86dc1143',\n    685000: '8ae92af68bcc012326889b5ef89b899aa38f65dfd3d9f7dd5b29dba0c5fbebbd',\n    686000: '5223ea0cffd3c59fa6597208425ab55b3a76366fbd27d22508cd208ebf2a2eec',\n    687000: 'e160e55af7fd110905364f980543fca62a123c84c81e9fd38a0aeeebc30a501a',\n    688000: 'fcf3b5e0afae8d665cd6f63dacd2a861aecac20f8b73a682f79cad9523c5c4f3',\n    689000: '772ffab484f07ad91ed45cf6f569eff7440f4243e16e07698e4b7bd4a109e6e3',\n    690000: '5b2a4fc69617ccf1787ee40a0f6d7e0af783bcd856c2c8e0ab747a91a7c68d19',\n    691000: 'e4ba352a759e2425d508a4d5b58595e6ff5eb912d8bfece5a0bc646f61e77084',\n    692000: 'fca2c5b6a721db85278ab55b5e2e39a445e5b1ee5ba69a17044623a6b945d0b2',\n    693000: '99f4365eee70f86499ec26c373922d389cbc5e2a198e96af5d5823ad241748a1',\n    694000: '0e19e76398e65cb01517dcd4ee702c9c04c0fd53cf9468ee539094aa6248e1c8',\n    695000: '5addd92022007d81ced43595458e2eee2903227063af8e9edd75cccbe559930d',\n    696000: 'ba25dfe3467cc9999eb8593d955032475b777cefc8006851f692266ebb83d140',\n    697000: 'e10c1c734038198a99bf970ae89e3a56b2058612e35eac8141d48e147d2d47e0',\n    698000: '472c4de8e57737ca48c3bdbf3c35c37f24a179f5dfa48af89efda3c3d33c131f',\n    699000: 'b359f2e61a3ca3cf8b613045617e38b06767ccff72129f13971faf26a4d08234',\n    700000: 'd0555d53358978ae0abcb09f911c1b3e7b5a282b4ff6d455ca9ad04299666dc5',\n    701000: '5c763b8b4329809553fb58ab279ebbc6639695c45760f0c181168d41da95e6d7',\n    702000: '8a6abdca484fbdac5fd6fb279a435377d964d43b62393b0d5b0cd3baaab1d2c3',\n    703000: '50cf159af75b28c7a95ca990e480ebcd534c4601b03d7c0a979f26f4e33cecd7',\n    704000: '0daf7f9c7eca5c6d91de9ebadb3ef1287c290700bab0aa9f6a2ac4bf42a8a98c',\n    705000: 'a359e7584112eadd7fad5d961f1dee180481852d7706701c2211a98a009c128d',\n    706000: '139ab9432b0c7f62739f818b114d57a59cbf20584230f1198c35f1da62c2ed62',\n    707000: '43e1f4c83d26d419a35adc4dc5bd85c6253ec0faf7e70196bc5d5fac18c51746',\n    708000: '1a05dba8d5d2ef4984c79cc939148ad71535043640fd5644141cdbc512623514',\n    709000: 'fe7a8bd03f742de5227e5d0c8dcfd0b5cc13582703b74b1898c506fc6ffdab04',\n    710000: '26217b4ecadd39e9a14ffb6cbcaab6ee7bf0954efcbbbec4de9bddb461819084',\n    711000: '94aee5cc00e862e4952e573908449f4a48b630285f2f2fb20765d01dfcb68ac6',\n    712000: '21fb256c0f5133634b94f6522cb8d2b0a9b982f1672ba561033be689410e7860',\n    713000: '8d7d7ee4cb0598fecd85c8004b59074bedc3219f2376966e01a7faa92377cdcf',\n    714000: '56a033531fc8dbb7ffe07d7365cce20281e522c3ff9fe7a19d0188af351a5799',\n    715000: '4a6671846cccdc26d6fd1d77db50772315f932c24dc706004e11de68ad1ac387',\n    716000: '4222447b25305bc063b668c0e010b16f9c0802fff9371e9d096ba2174926f073',\n    717000: 'c231fcf5238a34cd15ac735d27b3dd0ee025714e48c408a798f0ba74be0aef77',\n    718000: '0f96aaa30d20fc572d9db26871143a657b706c35a9668d31e3b64280a049787a',\n    719000: '8557c088c4fc4745674c795d4a58aeb3df0dc91bb5ff93cc6268c4a88c64db5a',\n    720000: 'd699e06ee7835c9d687853187125f27145ec91daf394fdee37218b8c34fee9f4',\n    721000: 'c675e891a36425627e20de36f1fbdd5baba7114661f1c45f81f66a3fc55da902',\n    722000: '0c6fedfb6d6c1a77254904fdd2400dedce7d45bdc2271beb77e2087a0ba30d1a',\n    723000: 'a442c320886beccb3d7ea13276dbef8e98e1a47686cba2cdbeb6a2d2883af928',\n    724000: '1592e2d6ac3be7535b44db6cc99080d00b19c6663f52eef3c28eae3dac27ba49',\n    725000: '45b2a800f17b8571172a2658577dbe95b91ae88e611a91ac0c92609b3600f693',\n    726000: 'ed6257a6567665747aa354e93ab7d3e6539d6dd41fced8a2f62cf848e2b30ce0',\n    727000: '4b1a577c6c2358b0344bb1befd9b8e5572b787ec2bdc0bdcd4a150f26b2e2ab7',\n    728000: '448765fbdf6261c376120ff9401db8a8841fcbed466365f982d3bc53775b93ca',\n    729000: '99c2acea0af193d2e10498acd1c6d162d2a804a69157af46817b5ece5ea86491',\n    730000: '94cec967e44f850f512d4240cb8a52ffaf953d0364b0a1dd7604b4a01406e669',\n    731000: '6e63f5019439bc7e27a17a189baad0da8f5724883af3ca35efa0d4e5aaa75b97',\n    732000: '53e1b373805f3236c7725415e872d5635b8679894c4fb630c62b6b75b4ec9d9c',\n    733000: '43e9ab6cf54fde5dcdc4c473af26b256435f4af4254d96fa728f2af9b078d630',\n    734000: 'a3ef7f9257d591c7dcc0f82346cb162a768ee5fe1228353ec485e69be1bf585f',\n    735000: '9bc81abb6c9294463d7fa12b9ceea4f929a5491cf4b6ff8e47e0a95b02c6d355',\n    736000: 'a3b391ecba546ebbbe6e05c5222beca269e5dce6e508028ea41725fef138b687',\n    737000: '0f2e4e43c76b3bf6fc6db9b87adb9a17a05e85110dcb923442746a00446e513a',\n    738000: 'aebdf15b23eb7a37600f67d45bf6586b1d5bff3d5f3459adc2f6211ab3dd0bcb',\n    739000: '3f5a894ac42f95f7d54ce25c42ea0baf1a05b2da0e9406978de0dc53484d8b04',\n    740000: '55debc22f995d844eafa0a90296c9f4f433e2b7f38456fff45dd3c66cef04e37',\n    741000: '927b47fc909b4b55c067bbd75d8638af1400fac076cb642e9500a747d849e458',\n    742000: '97fa3d83eb94114496e418c118f549ebfb8f6d123d0b40a12ecb093239557646',\n    743000: '482b66d8d5084703079c28e3ae69e5dee735f762d6fcf9743e75f04e139fd181',\n    744000: 'f406890d5c70808a58fb14429bad812a3185bdb9dace1aa57de76663f92b5013',\n    745000: '2bd0802cbb8aa4441a159104d39515a4ff6fc8dfe616bc83e88197847c78bcff',\n    746000: '24d090a7b6359db3d5d714a69ddc9a6f2e8ff8f044b723220a8ba32df785fd54',\n    747000: '07c4ce9ce5310ee472cf753ddb03c39c5fee6c910d491daffd38615205411633',\n    748000: 'ea913798c0f09d0a27eae7c852954c2c88b8c3b7f23f8fba26b68a3952d0ffde',\n    749000: '23f256adebfe35d49ba84ad49f3f71fc67f7745091c91f22e65f1cc2e23b8f2c',\n    750000: '96db12ee3a295f3d5c56d244e6e7493f58c08d3427e379940e5d4f891a41ec26',\n    751000: 'cedaf12415dac1314942e58ced80830b92fbfabc41f42a0b0f054f0672ef9822',\n    752000: '293606bcd9fbbee5584724301b2cf86bb69204820023e1fb46c238ddfbc660ab',\n    753000: 'f4d43cbb38b7d97919dedc0f5a6dc8007896c4f443b76f3e5693e25bc46760cf',\n    754000: 'fcaad22fd815311280fe451086516375d1d9d92b2990c7c351407df5aa19011e',\n    755000: 'b9276f10d1844cb5b0308766c8db960490ac34a73c4653d0a91202789a6ccb9b',\n    756000: '2fe5581f1110c1c8dcea46cad647551bd6bd640cb37738d863e189bd8f368347',\n    757000: 'b9d915f366f0b010429a52245b0fb02774157eb9fd8f66bce32dcd3acc71c2a1',\n    758000: '62d1854fc15db56b5d0e05ceeb54c1297966bf9dc7f7a0a14b42c059fc485d1b',\n    759000: 'f4ca9f69d16d092f4a0ea5102e6343b21204c4ea9cd9b22cddd77dbb5d68ade3',\n    760000: 'df3bb86641330d8cc7f55a2fd0da28251219e95babe960a308b18e08a7d88fc8',\n    761000: 'a93029475de4bc7569b6ae802d658cd91c84cc253772712a279f140a6c3b91b1',\n    762000: '307e289dc6ec8bcd62ca8831e4159d5edd780f2fae55ba55dd446225450f46f8',\n    763000: '293f73514abca24f374473bd0394179812952a04ea13dc60ef5ada5331fa274f',\n    764000: 'dd8b082db9281e3d9bacf15d6b352fda186d2d2923c7731844d0d4764dd71db8',\n    765000: '201239e562d2571bf47347b3522fff89632aecea3b2d8cef05151f88b2b0bcdb',\n    766000: '4a55a538b51b5650979e64521998cd5c5ad055ba9f3ac0e3e2a28febc6cc2798',\n    767000: '3916666f2adbb05ea98ec1961f9546b9afa0f6910ec95e42ce37267f2ae4f79c',\n    768000: 'dc0ad881eedcb5fd4954238f462080d6e7636b058d481698ed1c077e0ce2207e',\n    769000: 'eaf10a1e1ec6e129289b8479a05df03e0808f1f0946f1995de6524e9ebe7a461',\n    770000: '7200c64f22e32de7f999583361c933680fc9a2ffcb9a5ab73d3076fd49ec7537',\n    771000: 'd883111a2eeacff80ce31df35ab6c943805b9e48877b413fccf371e5dbfa7fb2',\n    772000: '3977d3c60edb9c80c97bb2b759b1659cbb650ad2d3a6f61d2caec83f1b2ae84c',\n    773000: '9c7175fb8646a1a82383b4c534fd01bcf92d65c43d87ae854d51a784b04dc77e',\n    774000: 'e0e92485f86e5fffa87b3497424e43b02a37710517d9d3f272392e8cdc56e5e9',\n    775000: '6395229113d3aa2105afbaeb8b59621a536fc61fe272314b2fc3bdda98dd66cc',\n    776000: 'b4b00207328b5f032bd4f0b634f91323ff520ada8c8bfec241b23c8e4bfd5a4e',\n    777000: '14cdc6f5f7b4bd5bad745dfe6fcd114e9194026412a2e1b3f345be2eef433d16',\n    778000: 'd3cd7b68be504c32117b670d38d59d44b02dcf3d65811efc2ca5531d902623cc',\n    779000: 'afcd220e4040cb5f92d4b38fc204e59822df2218f767f2c4b33597b238a35f77',\n    780000: '78252a9cfc289a70192ed8dd3dddeb1b9a4f9b8eff9a5d0ac259b3254472cf68',\n    781000: '02ebc3f17d947481a311b4771c254f1e002b6a9198d4a5258ce6c13165aadddc',\n    782000: '8dd9f1f372ee6d688a0bcdc3b342c77804ba5a646a218be4bc2aa02d846206c0',\n    783000: 'e46b0d02ec2ef488fae455665e107520e1bd2b4f35ca52af7ad8addd2f72fa73',\n    784000: '9ee8a8de94231e3ae3a610b82fdbca48dc14d9b80791d20af6c365a31822df6f',\n    785000: '21e1cc12def8173a50158b2833bd91a62140c61646f5e08aecaee3e6da20735e',\n    786000: 'b3e659f84d73de42888cc0f2b69bae71dd5fa6756a437a4b21958b182faa316e',\n    787000: 'a9be7ba00ea6a9ea6bd03d8412ec014ca7e8cda6bdc33382f165e702811b8836',\n    788000: 'a4c14729f8a68c03f5a0ccd890ac6a92b39c143f1f752fe81ad051eb52d8dce0',\n    789000: '5cf66d224e5645097efc9c3c0392b51c8ca8ea1295151921a7912a2f04ee1274',\n    790000: '676769ade71c33bc102bce416e66eb2c6794b03d7b8f5a590c87c380da463775',\n    791000: '0228e074451797bf6bfbc941bcafcbadc972d32e4e1e0c5da015513f65714217',\n    792000: '0fa3d00a1f19c5ac060e10a410cf7cea18eac5f89018d79ce51ac3fc66bbb365',\n    793000: '5f68d0868b424e32f5ce3d8e7d9f18979da7b831b8ef4e3974d62fb20ff53a97',\n    794000: '34508c56423739c00a837801b654b07decb274d02b383eff396d23c4d64bc0e9',\n    795000: '7f70910c855d1fd88cd7f9be8a3b94314ee408a31a2da6301404bf8deb07c12c',\n    796000: 'b74ab8813b1d2a0967fea0e66597572e5f0b5a285e21f5150fcc9d5f757de130',\n    797000: 'bba27b1491d907ab1baa456cb651dc5b071231b1b6ad27b62d351ca12c25dbfd',\n    798000: 'e75dcb15b2fc91f02e75e600dde9f6f46c09672533bc82a5d6916c4a2cd8613a',\n    799000: 'adf62c826a3e0b33af439a7881918ae4ce19c5fb2ca37d21243415f7d716aa65',\n    800000: 'd8f0ca13a8c8a19c254a3a6ba15150a34711dca96f2d877162cc44aa2acfb268',\n    801000: '2a8c7104c4040a2bc31913ae25e9361df5bac9477368c708f86c1ca640480887',\n    802000: '1f3b09d3561c4a8a056b263289bd492dc6c0d604c3fa195935e735d1c0ddc40e',\n    803000: '037769628c40a701fdb4b16d79084b8fbb319fde79770a7ac842f3cdc813099e',\n    804000: 'a0c6a089e5fa1e3589ca282085fe7201a5705776d81b257ffd252b2947fa6428',\n    805000: 'b2ac99bfc4a488e7b7624b31ee061991a6dd0881bb005cd13f3dd2e66a08fe19',\n    806000: 'ffe63cb999a278280b80a667d2dcb60c40e43a53f733914d8bec808b694ebf83',\n    807000: 'eddb09fc6c4869a59b520d0befb1fb6ac952333f3cc5de086539c85ea8558778',\n    808000: '0f4fb3f9172e52897ea992d9f3a2024126c4d2e63e9888739f11fb1f5e4c1f46',\n    809000: '9641dd720d23ced2f1cb6e5cf46ac4e547afb9f56263c4cf58e3b19d407cf401',\n    810000: 'de6dc953acd7e5ef213b3aaf1c4a9ee1d5b756bfce5525ee105214647e243a85',\n    811000: 'c52c83712ca12b24b2db1b4a575e7f352b1d560cbf702e121a03bdca9e8be23d',\n    812000: '83143734bb965318a53a38a7e403dcdb3e3fadedb01ab12c370417fc2a0655c0',\n    813000: 'e480deff10c5a84fc957e3aed936690e24b74dd08fa8858a8a953c2f7383b914',\n    814000: '810d33afcee07b9abe16c6cdc3a041038daa131c476b0daf48a080007f08b490',\n    815000: 'b4aeb9e16fddd27844b2d56bc2b221134039bb5642c9e9ba88372afbdeac3972',\n    816000: '86e73b67aae3d248011b8f66ed414cb8a9ba4b2a3cf7e32773cfbff055d719b7',\n    817000: '3ebb8b83752b48242016cb682f0f6bd14e15371bf1163a5933193eaa0edeb351',\n    818000: '4d925e17f642f220bbf317d3d5355d2f41fbce325f190f8c3b32dc0b337d24d6',\n    819000: 'b9cc126d620f6b99d90a00d35957b0e428aaaa7c986bc9e816a60e4334572961',\n    820000: '9c2f8c142bed1f94dca29276f7c83958be8cfe11773bb9b56c808fbcf7d3b1f8',\n    821000: 'e5509eb98895cfa12a8da5d54c1df3f52472ffcbdf707adbf84a4a9c5d356203',\n    822000: '764aada4802ebfe4ef935ab50af06a4f83aa556c49fdde3d9e12e1abd230c16b',\n    823000: '1dbd745c2e96a365d865f990d109137d32d42977f503af55d8c00b109d31d3c3',\n    824000: '954304a0b0c8f549c3bffd5ff46b5b8f05b0f0fde2a36f24fd5af9d774fb3079',\n    825000: '17808b14f2056c1a5d46cb7617e9de9be6a1a6084edbc1bdb778586467a72297',\n    826000: '3ca1167d4cac8b187829b23001b438617c43704b42462c4eb001b0d434cb9651',\n    827000: '246d1607245e4a202f420393ac2e30e9cbf5eb5570dc997073b897f6d8643023',\n    828000: '1764730a8dc3e89d02d168ff6bb54e8c903820b74711af6ff27bd0c8545577e7',\n    829000: 'd9f3ab0cd823c6305bd8b95a96188bb4f2ca90b4d66c5d12293e8b6192bac0f2',\n    830000: 'd4ff51f0092b04aedf8d39937680d8e8309b1be21d36e7833ed36f8e30aad6ea',\n    831000: '3e92e76721b962396dce52993fa7606552f0907b38f7b2bd7b21ada98c145f47',\n    832000: 'df12fcdb4cbe53ba627ace6de898298de175f8671d3d90170732d110fcdc34b8',\n    833000: '25167ff38ae4a5964b618cabe0a12d4de62ac7a4c47448cdb4499e09e108d5b9',\n    834000: 'd31f5309ea179a1e386e835fc372e47dcda6871a3a239abfba50c4f368994f13',\n    835000: 'aff7e8dd3e55ea807fcbe284014075f420b3a23f1b0eb47bacdc1c91d2899813',\n    836000: '3b5ac6d64c470739bb17d1544a285affb40f2d33e92687e5ba7c5ac602e0d72a',\n    837000: 'd5619cbfe4f27c55f2bf9351b4891636cf64fef88212a5eeeae7bd3de47fe0bd',\n    838000: '1f9102a49c6ac470cb5d0050e5300b1443840d6d65719b835e3bea484aafb2ec',\n    839000: '3f63e391f0fbc5787fbe4ace3bada3816261294ea1c6ee435001801023682f90',\n    840000: '777894fd12bd0d6dee7bcde2995c68e55e7094e3122da38571e4b6c4304b75e0',\n    841000: 'ceb0c598c788e25e43e25aa4beff5c7377035824844cf1675eaea537074df028',\n    842000: '8661cf2065dc713d2ba043f0b81f0effcc940eeb3e91906a21ff22c210561dcd',\n    843000: '0dc2766f90415009d0c86bedffee6ebcf58042eb08262c0c67c4e9ed86b2aec8',\n    844000: '26d072da864cab268a12794977b04ec44fb69ef3978e2342e82225974dac54dd',\n    845000: '95e93bb60be8d5f07a1f4d26290c914957a82fc9d26ae8a3f20082eda27406ff',\n    846000: 'f1bdc39af7705e58ab8b6c31dc70dce1e115db1cfd8cc9b037949dfbec82a59a',\n    847000: 'f5f10f06396ecf2765d8a081141d489737c1d8d57c281f28f57c4cb2f90db883',\n    848000: '331b8ef08605bae8d749893af9ed54f0df4f07a5a002108a2a0aea82d0360979',\n    849000: '75b5f6233ab9a1bbc3c8b2893e5b22a0aa98e7ea635261255dc3c281f67d2260',\n    850000: '5d7e6fe83e0ea1910a54a00090704737671d6f44df4228e21440ad1fc15e595f',\n    851000: '7822db25d3ff0f6695ee38bad91edf317b5c6611673d28f1d22053110bb558be',\n    852000: '2f0effad83a3561fc1a2806a562786a641d9ddb18d16bb9308006e7d324a21e9',\n    853000: 'f603b2eaff11d5296377d990651317d40a1b2599ad2c5250eab131090f4b9458',\n    854000: '34d59b26a50f18a9f250736d0f2e69d28b7e196fbef9b8a26c6b0b75c16aa194',\n    855000: '76dd1ffff3946c0878969886fcf177ce5ab5560df19ddf006f9bcb02ae3e4e4f',\n    856000: '74ff0b6f64e9dd5802fec2aac1d3ae194d28b9264114adaf0a882b46c8c918fe',\n    857000: '7b5badfa2e4f40aa597a504d7ebe83c3705a2c6169a8c168ce293db223bc2d32',\n    858000: '2bb0767a0f72b20d45ecfc3e34517dbda16d85758e040cf0e147f4cbd0cc57ac',\n    859000: '3d741b9c365a91ed76f85824b94d19ec19b608d232660840ba59c7aa4b2cb67f',\n    860000: 'd481a5a117878c0e3acd1f5844e150fb30e617577947d9846b1d214d703b71b0',\n    861000: '54033424e488a3f1ad6946d4a6d9acb48465d6b1dbe8e1c2504a54cc84d7cad4',\n    862000: '464bc3820a8cc8844dc9e26c388009e9982c656d46ef4b4fd0a2cb0e4eea0aaa',\n    863000: 'd1aa94be2174f66780c4f226b9da3f6712b0f37af8dec33360bea83ca261b342',\n    864000: '8c16008f11de5bc395d88cd802514ff647450f1bc136724b9aaf2ccce10a494f',\n    865000: '3dae86012e97a201e2e1a47c899001ac00f78dc108026ed7c4194858c6c6dd5a',\n    866000: 'afe5b0ccab995e1a1fa25fbc24c1d4b1a92c43042d03395f8743dcd806e72fd8',\n    867000: 'c83716ac171aa9ab0d414833db340fa30e82bfda6cc616d3038529caab9b5600',\n    868000: '8c409fe03cd35ef2d8e366818788b40eaeb4c8f6ae91450d75f4a66ca5f69cad',\n    869000: '1d47909ceba790b8e1ce2e9902ee2775ea99e58efdb95668f9803a8ccf95f286',\n    870000: '9adf5da1476388f053aa42de636da169d1cf1c9652cdf7cd9ad4fb18a0eb3388',\n    871000: '8ad57fb1e74bcba0b5614fbac003be2bb32275dd85b38f2d28a0585005a99cfc',\n    872000: '84a32e92012a356106e9657da8dab1a5491ea588fc29d411c69b20680c666420',\n    873000: 'adf5921bbbfaa43929f67e6a070975313b77b456e262c700a27be611fceb17ae',\n    874000: '09eaa7c4b18c79a46a2895190333f72336826223d5c986849a06f5153f49f2a5',\n    875000: '235d7e4f31966507312149ea4c5e294aa84c695cf840117f0ef5963be7a0bda1',\n    876000: '9aa9cb806ccbec0475ac330b496c5b2edeba38ba3f1e13ddd54a01457634a288',\n    877000: 'c1e7f9b2b20bb1c4c0deadbc786d31fdf36f262325342aa23d1a66e2846b22bc',\n    878000: 'ee0d2b20ac28ce23ab38698a57c6beff14f12b7af9d027c05cc92f652695f46b',\n    879000: '0eb0810f4b81d1845b0a88f05449408df2e45715c9210a656f45278c5fdf7956',\n    880000: 'e7d613027e3b4ca38d09bbef07998b57db237c6d67f1e8ea50024d2e0d9a1a72',\n    881000: '21af4d355d8756b8bf0369b2d79b5c824148ae069026ba5c14f9dd6b7555e1db',\n    882000: 'bc26f028e547ec44fc3864925bd1493211773b5cb9a9583ba4c1909b89fe0d33',\n    883000: '170a624f4be04cd2fd435cfb6ba1f31b9ef5d7b084a25dfa23cd118c2752029e',\n    884000: '46cccb7a12b4d01d07c211b7b8db41321cd73f30069df27bcdb3bb600c0272b0',\n    885000: '7c27f79d5a99baf0f81f2b09eb5c1bf905976a0f872e02bd4ca9e82f0ed50cb0',\n    886000: '256e3e00cecc72dbbfef5cea627ecf1d43b56edd5fd1642a2bc4e97c17056f34',\n    887000: '658ebac7dfa62bc7a22b1a9ba4e5b425a866f7550a6b40fd07de47119fd1f7e8',\n    888000: '497a9d02868605b9ff6e7f15948a83a7e07606829107e63c2e091c90c7a7b4d4',\n    889000: '561daaa7ebc87e586d37a96ecfbc72484d7eb602824f38f484ed333e78208e9e',\n    890000: 'ab5a8cb625b28343f8fac858eab6576c856dab88bde8cda02b80b3edfd307d71',\n    891000: '2e81d9fc885ddc09222b298ac9efbb73638a5721802b9256de6505ecf122dbaa',\n    892000: '73be08881b8832e986c0bb9a06c70fff346edb2afaf69630e47e4a4a90c5fece',\n    893000: 'd39079dcaa4d8af1c26f0edf7e16df43cd857a31e0aa4c4123226793f1ab497f',\n    894000: '0a3b677d72c590d4b1ff7a9b4098d6b52d0dc10d64c30c2766d18e6eb02872cd',\n    895000: 'a3bbba831f48c5b68e494ee63015b487782c64c5c24bb29436283360c28fd1e0',\n    896000: '20af178a192ca43975ab6c838fe97ca42ba6c682682eddbc6481efd153ecb0a2',\n    897000: '8d0ee14b9fdb853a09ab2951d26b8f7cb8bc8038b09513bd330ee4b0bdcc4780',\n    898000: 'c97fbb70f804408b131a98f9fb4c04cdf2df1655d3e8ff2e0d58ed8537349f4e',\n    899000: 'eba2be80478e8dec2d66ca40b853580c5dad040351c64c177e3d8c25aff6c1b6',\n    900000: 'c4dc344a993558418b93b3f60aaef0030e2a4116086577fbf1e2f544bdbddae1',\n    901000: '36d84229afa63045875fc8fea0c55de8eb90694b3a37cceb825c87abf1fea998',\n    902000: '8ca4890ecfc5e3f9d767e4fcdf318a1e3e3597675bbcfe534d64e76bc4e8fbf4',\n    903000: '8b9f6a7514033c57668ca94fb3758cc6d1ef37ac982c2ff5a9f0f206fcd8d0a8',\n    904000: 'e9ae813991f35ca89af2fe1f1b6adf9e93c6b1dd6a74f003ebbe699a30b252ea',\n    905000: 'd426489d01d4f4c829f2eb68a67721d2c0e1c71e8c33ef9253593447e8603462',\n    906000: '63000bbed97451e68d64485c02c1c3d90b4156237dac315f4e012ffb538e375b',\n    907000: '96759653a4e514541effa7ef86d9f22a272ddde7b069149d17e9d9203a1edafb',\n    908000: 'eec6477d2f3b71bde76dc2380d6e06aa8aa306ca56ba1dd15a31c22ae0db501b',\n    909000: 'd5c2984cf130335aa29296ba5b17672d00360fe0ec73977326180014908c0b55',\n    910000: '7b99cb1c94144f606937903e173bd9ef63bfffd3db8110693fa4c2caa0abc21f',\n    911000: '95eed0d9dd9869ac6f83fa67863e77f24df69bcb90fef70918f30b2400e24ea8',\n    912000: '34c3c8780c54ecced50f0a6b394309d09ee6ce37cd98794699c63771d1d91144',\n    913000: '536052ddcd445702160288ef3f669ce56868c085315556c9f5ca081ef0c0b9e1',\n    914000: '1bcd1fe9632f93a0a1fe7d8a1891a4fc6ef1be40ccf887524a9095ed7aa9fa44',\n    915000: '139bad9fa12ec72a37b62ad8511300ebfda89330fa5d5a83861f864b6adeae67',\n    916000: '81d15282214ff83e2a034212eb58abeafcb5664d3734bff13b22b4c093b20fea',\n    917000: 'f31081031cebe450e4450ef397d91790fc0068e98e6746cd0aab86d17e4448f5',\n    918000: '4af8eb28616ef0e859b5471650c7f8e910cd692a6b4ff3a7171a709db2f18e4e',\n    919000: '78a197b5f9733e9e4dc9820e1c79bd335beb19f6b87056e48e8e21fbe27d83d6',\n    920000: '33d20f86d1367f07d6731e1e2cc9305252b281b1b092403133924cc1052f501d',\n    921000: '6926f1e31e7fe9b8f7a81efa73d5635f8f28c1db1708e4d57f6e7ead951a4beb',\n    922000: '811e2335798eb54696a4b11ca3a44b9d79486262119383d542491afa9ae80204',\n    923000: '8f47ac365bc380885db809f2818ffc7dd2076aaa0f9bf6c180df1b4358dc842e',\n    924000: '535e79802c10630c17fb8fddec3ba2bf85eedbc0c076f3575f8189fe887ba993',\n    925000: 'ca43bd24d17d75d55e72e45549384b395c62e1daf0d3f58f296e18168b918fbf',\n    926000: '9a03be89e0725877d42296e6c995d9c48bb5f4bbd971f5a9add191af2d1c144b',\n    927000: 'a14e0ef6bd1bc221dbba99031c16ddbbd76394186677c29bdf07b89fa2a6efac',\n    928000: 'b16931bd7392e9db26be975b072024210fb5fe6ee22fc0809d51980aa8068a98',\n    929000: '4da56a2e66fcd98a70039d9061ea5eb0fb6d9460b437d2191e47441182419a04',\n    930000: '87e820e2237a54c4ea100bdd0145598f05add92185cd3d0929aa2d5099f4d5e0',\n    931000: '515b22c91172157c443a47cf213014aff144181a77e276e291535ab3762bb1ae',\n    932000: 'e130c6a9eb416f96256d1f90256a148957daa32f56af228d2d9ce6ff27ce2011',\n    933000: '30c992ec7a9a320fb4db260373121efc7b5e7fc744f4b31defbe6a7608e0749e',\n    934000: 'ec490fa0de6b1d78a4121a5044f501bbb3bd9e448c18121cea87eb8e3cadba41',\n    935000: '603e4ae6a6d936c79b3f1c9f9e88305930953b9b390dac442976a6e8395fc520',\n    936000: '2b756fe2de4328e598ed511b8828e5c2c6b5cdda1b5e7c1c26f8e0424c81afa9',\n    937000: '1ae0f15f14a0d4819e34a6c18de9428a9e43e17d75383bffa9ffb18358e93b63',\n    938000: 'cbd7001825ec87b8c6917d6e9e7dc5c8d7767788b6ffd61a61d0c612dbe5de66',\n    939000: 'd770d0395aa79076044783fb37a1bb173cb95c93ff1ba82c34a72c4d8e425a03',\n    940000: '3341d0a0349d091d88d233cd6ea6e0ad553d52039b4d47af51b8a8e7573a7916',\n    941000: '16123b8758e99344ebe6670cd95826881b274c31d4da2a051052955a32bade3a',\n    942000: 'ac7430961e77f902918fe79a52cbf6b523e3f2804ec83d0b17908e131ea9ea68',\n    943000: '2ad08a6877e4687dcb7a623adeddc88403e8082efd6de28328b351282dc141e2',\n    944000: '81382e8c1f47fa7c03fa1726f9b09ed1cd38140fe50683896eaa1b403d7e5fe3',\n    945000: '152bfbb166da04dab16030af28ae65b3275819eed1d0bbfc11eba65616ebefd6',\n    946000: '25b3da0962f87a0d3e4aec8b16483efbcab9514893a42fd31f4cb544ddc45a1f',\n    947000: '2cb738ba342436628ff292797e3d36c4752d71bdc1af87fe758d469d06e36e0e',\n    948000: 'b3683e18570fcc8b986720514539181ec43fb5dbc20fe314c56ab6bd31ab766a',\n    949000: '94ced5bfba55ccffc909bf098d537e047d8d4cbb79f5e2a74146073f39804865',\n    950000: 'b11543cd2aedae27f6ddc3d2b431c897fdcfe59ed3c926b0777bc1e99de4d12a',\n    951000: '21508881a7f80fcd0b9b27bbcfba634b39c6525f5313968c4605cd55b4fec446',\n    952000: 'f9b3ed919c9ca20cd2927d899ee7a86c93c2dd919dafb6fdb792f2d9f1895cb0',\n    953000: 'cf578d8e80eec4102dc1b5321f10b36020b3b32f4b5d4664c90c412ca2ef6b42',\n    954000: 'ed17c919ae5c4be835966b47f667d6082c75917b95584b2d2aff0e32f5c8aa98',\n    955000: '948ea467fa01a20122e2146669214fdd3bb025038554609f7299ece5bca63e39',\n    956000: 'b50ff4c02957ed8764215d25f206f6f1fe6d0eb712a378b937ff952dd479afd2',\n    957000: '169922a3e51517ba6104a883d29aac03a9d20b4d448bd2773137b0d790e3db6b',\n    958000: '92258ac2e8b53167dc30436d93f385d432bd549711ab9790ba4e8263c5c54382',\n    959000: '7ca824697459eb302bcd7fba9d255fb269555abe7cf9d2dd5e54e196d751e682',\n    960000: '89f9ec925d23698076d84f9e852ab04fc956ac4465827303de0c3bb0b685eb32',\n    961000: '41cf75cd71bc12b93674c416e8b01b7410eb9e09eb8727ad93ff0b833c9966c9',\n    962000: '7db1f1dbff3e389713067879bfedf9513ec74bb1e128b13fc2fe23ad55fd0306',\n    963000: 'a35e71c611b2227adeac824d151d2f09bdbecd5765a4e62c6e74a3e4290abc66',\n    964000: 'dc1811130e249d2208d6f85838512b4e5482efb0bd2f619164a68a0c60d7f248',\n    965000: '92f5e25dd1c03102720dd0c3136b1a0769901bf89fcc0262a5e24405f349ca07',\n    966000: '08243d780d8ba96a940f409b87d9c6b8a95c92804173b9156ada0dad35b628dc',\n    967000: 'cb769a8935bb6faeb981da74f4079babbbb89476f825cc897f43e79790295260',\n    968000: 'ff3fc27d2998f4dc4ac1ff378afe14c7d0f43cc328deb9c978ec0e067d1dfaf9',\n    969000: 'e41a3452f45d5f025627d08c9c41017679e9c4804371dd1cc02f3ed49f85dbb2',\n    970000: 'f5eaaf7ba6b47245a4a8096a7785c7b25dc6db342ac2ccbba0c321e97ab58284',\n    971000: '75414062f1d4ed675dadc8f04ba10147a484aaca1ae316dc0b896a92809b3db6',\n    972000: '5bcf2ee00133774c7d060a1a1863dfccc20d5127ecb542470f607dec2504fe6f',\n    973000: '07d15b9656ecde2cd86a9d22c3de8b6505d6bab2aa5a94560b0db9119f1f6f6c',\n    974000: '2059e7924d7a210a88f5a65abc61152506a82edccd27416e796c81b9b8003f13',\n    975000: '7fcf5d8b2c0e51cfbdaa2502a9da0bdb323646899dad37dacc39af9f9e16fc5c',\n    976000: '02acb8cf87a0900436eccfca50371948531041d7b8b410a902205f84dd7fb88e',\n    977000: '2636dfd5a47016c893265473e78ecbf2000769d886f0d01ee7a91e9397210d15',\n    978000: 'ce92f52a35096b94bea73a7d4e113bc4564a4a589b66f1ab86f61c822cf9ee76',\n    979000: '21b8102f5b76be0c8e20d537ebc78ebe46bfcea6b6d2dda950ce5b48e85f72d7',\n    980000: 'f4df0bd63b36105705de62266d654612d9804bad7069d41344de269657e6f084',\n    981000: 'f006cd2718d98d774a5cd18394db7744c812fa149c8a63e76bab934aee89f571',\n    982000: 'da5d6609265d9153022d823b0260aa07e7511ceff7a3fd2ca7ce83cb3900a661',\n    983000: '3a26f3f02aa145fa8c5268fbe10dd9c3546d7dda57489ca5d4b161beb0d5a6e2',\n    984000: '968e8cd37a1137797d40f39f106cae62d1e252b46c7473b9434ad5f870ee88fb',\n    985000: '3129c3bf20deace1a9c92646a9d769da7a07f18dcd5b7a7b1e8cf5fd5390f8e1',\n    986000: '6ce830ca5da322ddbb97fc572ea03218913d070e5910516b33c6113b02b23c21',\n    987000: '7fb1a8635623847132ab766a99b792953379f782d1115b9649f5f9c5a742ca04',\n    988000: '5e8e6c6da7f271129c20c4dd891dcb1df4f9d690ee7cf391c6b7fbd028a0da4c',\n    989000: '12919e34bb9a9ac1d2a01e221eb8c511117fc4e1b3ae15355d95caf4673bdb08',\n    990000: '016f8b18227a0c09da55594a98638ad5b0fbb4896e2ab6163ac40b6015b2811e',\n    991000: 'ddf8cd6e2f4ee07530ae7567cef4fa2c2fd4a655cb20e20422e66fd49bde6489',\n    992000: 'dca77707c0caa3a9605f3dadf593402339c29448869907fb31f6c624e942dcbd',\n    993000: 'de9acc4c7c482ecac741fd6acbbc3a333afab52f3fe5eea4130c0770299a56dd',\n    994000: '54420631f8a801a1b8f391088f599ee22cedc06f24bf67f18272feb8fe70c682',\n    995000: '4b44b26e3e2495716dfd86fc42594cd4b1e4b70bdab4f0905cce4cb9556e008a',\n    996000: 'd6e41fd301fc5f519c343ceb39c9ff845656a4482e4e182abdcd3963fd5fde1c',\n    997000: 'd68b6a509d742b182ffb5a98b0e585a2320a5d3fe6977ad3e6cd06835ef2ea55',\n    998000: '1efcdcbadbec54ce3a93a1857253614536c34f05a0b1924f24bff194dc3392e1',\n    999000: '10a7713e46f47527f3819b4a9257a03f3e207d18e4917d6bcb43fdea3ba82b9a',\n    1000000: '1b4ddb1436df05f07807d6337b93ee1aa8b600fd6a910a8fd5313a39e0440eec',\n    1001000: 'cde0df1abdae26d2c2bdc111be15fb33231c5e167bb8b8f8eec667d71379fee4',\n    1002000: 'd7ce7a96a3ca73a4dfd6a1780e23f834f339142519ea7f45d256c113e27e4857',\n    1003000: 'b1a9b1c562ec62b9dd746d336b4211afc37482d0274ff692a44fa17ac9fe9a28',\n    1004000: '7afd6d0fb0014fbe16a31c84d3f1731736eaeef35e40bb1a1f232fb00345deae',\n    1005000: '4af61ce4cda5de58277f7a67cadea5d3f6ce56e54785b188e32306e00b0414df',\n    1006000: '08e1fb7295efd4a48cb999d899a3d481b682ddbce738fecd88a6d32cbe8234f0',\n    1007000: '14a367a41603dd690541daee8aa4a2882260059e3f85bd8978b7431e8f7db844',\n    1008000: 'e673230e62aaefad0678611f94ff35ee8a6e18eb96438bdfb4b614f54f54dba7',\n    1009000: 'e191af8fb71d0d91419abd19443af3d3f23ee4fe359bb8c390429cc838132bde',\n    1010000: 'ffdba58f184cf60838b75b7899b6633e7cfd34cf36eded572c0133d07387bc49',\n    1011000: '40801af3a5546cb9d53e05e21b74be09de9a421b762ca1d52d2266f5c2055ce8',\n    1012000: '552519acebed0e38102f5270dc60b1da7a123600b6b94169ae74462ae454693f',\n    1013000: '1eee96f48418929927eaa9642777bc806d326cfffaf077bc8695a7ecd438d631',\n    1014000: 'a471093e1de2a8db586412d7351c8d88e44ea890f46e9b43251af427a0a4a879',\n    1015000: '57532f5a522295cc139f008bdcb7a1e6d02e6035d5221b2687c7c216f06297a2',\n    1016000: 'ec46dba07addcb6e62f58456a53c513d876f1c49ae7d76d230adb8debd26027d',\n    1017000: '33ea8d25f342a7465ed71e4bab2b91007991e0994c61d321e3625301a1390322',\n    1018000: '4871c03cc95d4ce0a39bd2cebbb001b2ea1cce1b3561bb841d88f43bb9d12ffd',\n    1019000: 'f5248257576eb2ff4139d6374cc7ce34121cc942598cf9e04d2bd572e09189bb',\n    1020000: 'e7785286897c85cfb0276957bff216039eeb11bc1ebca89d0bb586022caa5750',\n    1021000: 'a30220f17d060634c5f6a1ddc5ea34b01c18fb5eb7e0e8267b66bf5a49525627',\n    1022000: '6083ea49e64ac0d4507c674237cf87d30b90b285ec63d082e626df0223eb7c9c',\n    1023000: '1dc5596d716bc33ee0f56fc40c1f073155a58a7692935c9e5854ef3b65b76828',\n    1024000: '065adfee40dc33abff07fb55339571712b959bc1830dc60b6691e36eab1508ae',\n    1025000: 'bb6903752d31278570e774b80a80782179c78f099e58c3dc4cba7afea7a471c4',\n    1026000: 'f3050f3c2f3a76f5084856b0f089383517caa3f51530fbc29335308f5f170625',\n    1027000: '746ed3701510d07958d11a06f22dbb839d9858373dc5a33249dd69e91bab01fd',\n    1028000: '43f7a96ea6a45b78c29ad4a2f8680ef184438c2bd3686172b0564e0ae6dd7ba1',\n    1029000: 'cbb9916099c59e14fe61d284374f4feaa3d43afec59e4698ed92143576f24b34',\n    1030000: '2e805fc2331e32e586ea692bc3d4e6b11e1ec3f1cab6e331b459f9f1ac9a1f1e',\n    1031000: '04f324f8f6d4f9901cf65f78dc91d6010ea6cf125f5ac0253b57b5f1f79e81e0',\n    1032000: '60ca62f52fdfd858b0ee0fdb380648bde85ca14e2a73565205ed4ee0bc861c77',\n    1033000: 'eb60aac23d599d3099cf98ed8fc3213f1bc06bc1c677429b303e9c81f79f1340',\n    1034000: 'f0328df2daf119ce673ddfa7a39a84576985f701f7a7dec3f56f58c2019ebd4d',\n    1035000: 'f9d3cbce3854de168d8835c96917c01be6244c8f82641e8d9398dfffec4e7107',\n    1036000: '7dca97e6e1d6ed70aa7805f74b768009a270e7ebe1dd951e8727d1d2f2d271f2',\n    1037000: '5329504126b2845b3044f423b521e77ff58d7d242f24bf87c87f4d8d4e03a947',\n    1038000: '5bad3ad55e3daa415f3182a1f2a099fe1767e8fae34e9bb95d47e242b8971434',\n    1039000: 'c29729b8ba49ac0043fe4aa6fc971f8ac3eda68ff92970957ada39a2989b2491',\n    1040000: 'f303aebfc9267600c081d0c021065743f93790df6f5c924a86b773788e0c45be',\n    1041000: 'a1cbe5059fa2275707785b77970c36d79b12c1ba93121bc9064ab9b64abacf7b',\n    1042000: '004b0dd4e438abc54ae832d733df32a6ba35b75e6d3e0c9c1dee5a7950507295',\n    1043000: '31893a3fe7bb4f6dd546c7a8de4a65990e94046aab442d18c68b6bf6acd54518',\n    1044000: '2c4dd479948acc42946f94050810000b0539864ad24a67a7251bff1c4971b035',\n    1045000: '1cea782d60df35a88b30ae205ce37e30abc7cad2b22181722be150bd92c53814',\n    1046000: 'ee808f0efb0f2ef93e8599d8b7f0e2e7c3cdc42353e4ea5165028b961f43d548',\n    1047000: '75f057e2a8cb1d46e5c943d63cc56936a6bac8b1cb89300593845a20baf39765',\n    1048000: '2abcd227f5314baed85e3c5b49d3888a60085c1845c955a8bf96aa3dd6394798',\n    1049000: '5d0ec24b9acd5ab21b42f68e1f3142b7bf83433b98f2fa9794586c8eff45893e',\n    1050000: '1d364b13a4c17bd67a6d1e5f77c26d02faa014d7cd152b4da70380f168b8e0ff',\n    1051000: 'b9a20cec21de84433be9b85817dd4803e875d9275dbc02907b29888431859bae',\n    1052000: '424cb56b00407d73b309b2081dd0bf89213cf024e3aafb3090506aa0ba10f835',\n    1053000: '6df3041a32fafd6a4e08778546d077cf591e1a2a16e77fe7a610efc2b542a9ff',\n    1054000: '78f8dee794f3d4366019339d7ba74ad2b543ecd25dc575620f66e1d535411971',\n    1055000: '43b8e9dae5addd58a7cccf62ba57ab46ffdaa2dcd113cc8ca537e9101b54c096',\n    1056000: '86b7f3741343f85d93410b78cc3fbf03d49b60a664e908703016aa56a206ae7e',\n    1057000: 'b033cf6ec622be6a99dff536a2cf73b36d3c3f8c3835ee17e0dd357403e85c41',\n    1058000: 'a65a6db692a8358e399a5ac3c818902fdb60595262ae05531084848febead249',\n    1059000: 'f6d781d2e2fdb4b7b074d1d8123875d899cdbd6be375cb4288e86f1d14a929f6',\n    1060000: 'cd9019bb1de4926cca16a7bef1a46786f10a3260d467cda0775f73361795abc9',\n    1061000: 'ed4f5dc6f475f95b40595632fafd9e7e5eef388b6cc15772204c0b0e9ee4e542',\n    1062000: 'c44d02a890aa66979b10d1cfa597c877f498841b4e12dd9a7bdf8d4a5fccab80',\n    1063000: '1c093734f5f241b36c1b9971e2759983f88f4033405a2588b4ebfd6998ac7465',\n    1064000: '9e354a83b71bbb9704053bfeea038a9c3d5daad080c6406c698b047c634706a6',\n    1065000: '563188accc4a6e311bd5046516a92a233f11f891b2304d37f151c5a6002b6958',\n    1066000: '333f1b4e996fac87e32dec667533715b31f1736b4342806a81d568b5c5238456',\n    1067000: 'df59a0b7319d5269bdf55043d91ec62bbb30829bb7054da623717a394b6ed678',\n    1068000: '06d8b674a205393edaf20c1d837baadc9caf0b0a675645246263cc163302241d',\n    1069000: 'ac065c48fad1383039d39e23c8367bad7cf9a37e07a5294cd7b04af5827b9961',\n    1070000: '90cd8b50f94208bc459081356474a961f6b764a1217f8fd291f5e4828081b730',\n    1071000: '3c0aa207ba9eea45458ab4fa26d6a027862592adb9bcce30915816e777dc6cfc',\n    1072000: '3d556c08f2300b67b704d3cbf46e22866e3ac164472b5930e2ada23b08475a0f',\n    1073000: 'a39b5c54c24efe3066aa203358b96baea405cd59aac6b0b48930e77799b4dd7d',\n    1074000: 'e8c8273d5a50a60e8744716c9f31496fb29eca87b4d68643f4ecd7ec4e400e23',\n    1075000: 'b8043ae41a1d0d7d4310c85764fcba1424733df347ffc2e8cbda1fe6ccbb5153',\n    1076000: '58468db1f91805e767d334824d6bffe54e0f900d1fb2a89b105086a493053b3d',\n    1077000: '04a78749b58465efa3a56d1735cd082c1f0f796e26486c7136950dbaf6effaa4',\n    1078000: 'e1dd6b58c75b01a67d4a4594dc7b4b2ee9e7d7fa7b25fd6246ce0e86eff33c75',\n    1079000: 'd239af017a6bb664485b14ad15e0eb703775e43018a045a8612b3697794460da',\n    1080000: '29ae5503f8c1249fefeb63fd967a71a70588ee0db1c97497e16366163a684341',\n    1081000: '05103ab27469e0859cbcd3daf42faa2bae798f522534697c7f2b34f7a050ee0f',\n    1082000: '4553d2cb7e90b6db11d242e287fe96822e6cd60e6388b94bf9006411f202ba03',\n    1083000: '97995acd178b2a142d571d5ae1c2a3deaf93a909fd91fb9c541d57f73e32dc99',\n    1084000: '9e3f23376af14d76ab24cd54e321dec019af73ad61067d959ff90043acc5ffcc',\n    1085000: '81c056b14f13cee0d6d6c8079fdd5a1a84c3a5c76cc9448612e8ef6d3531300e',\n    1086000: '8a0004f6809bdd075915a804e43991dfe8f22e05679d2fdaf8e373f101bac5c2',\n    1087000: '27c45a4c9ad24e038f2ebe40835a1c49ac7221d7185082866ee354351ba87c7a',\n    1088000: 'fd27e21747117b00b4ada1cba161ac49edb57cca540f86ac5ba885050f08f824',\n    1089000: 'bff867335767103bc3ed15ede5b9fde88016f8ede15dc5bf3e81ea40dcfc61ae',\n    1090000: '608f75016d1db08888dd59640f63e838c19bdfa833c0cc177ad3d2b818b0db5b',\n    1091000: '90750b452bd4dedaab6b57fecbfe88f71ce3d5437fad7f9ec0fdd270445c7526',\n    1092000: '98287b39f9f1233017dc5d932e5c77f0521ca84587eb3f39f0e7b6c297c749af',\n    1093000: '68a5846ed05c9bb142197849106838765f90f15c10b2cc938eef49b95eaa9d33',\n    1094000: '5660a1aac2fc763a417fc656c8887fc8186bf613ae1ccbb1a664fb43ce1fa1d6',\n    1095000: '62bad3db418b3f4cad3596881b645b72479c71deb0d39c7a4c8bd1577dc225fd',\n    1096000: 'e0e4b2b183591f10dd5614c289412f2fb5e320b7d3278f7c028f42f591872666',\n    1097000: 'a233a233fc2aa5dab9e75106d91388343ef969458ea974f1409a2ab5fc441911',\n    1098000: '16dfa5fa6cbd1188e562697b5f00ac206960d0851ed84adf37ae975fd5ffdd6a',\n    1099000: 'b8a870b7dc6d3263730c00f59d52aa6cce35dc59aa8fba715034cc2d14927260',\n    1100000: 'a3cd7749743da22a3846dcc2edbf1df21b938e829419389e3bc09284797c5b43',\n    1101000: '75b14c2a95e2a095949729b7c0b624bd725a2de98404a8e3247b60c977d0198e',\n    1102000: '4d3af64d37064dd5f57e25d61f248a1e21c1b1cadd7bb1404e35c9fbe06f1fd4',\n    1103000: 'd73c92bfed358dfcd7659228974ab75ea2fc86f2301ee47133adad8075203872',\n    1104000: '30cd82354f37bc0b412123867c7e1835206022a7501853bf8c0d3df02f291645',\n    1105000: '1d2ef984f26693dce77460cd2694e5da46e675077e91a1cea26051733b01a7ef',\n    1106000: '51c076c304222fe3ca308ba6968c46fef448f85be13a095cecb75b90e7954698',\n    1107000: '99e2221339e16acc34c9816f2ef7b866c2dd753aa3cbe484ae831959a23ece68',\n    1108000: '0f1227c250296bfe88eb7eb41703f99f633cfe02870816111e0cadfe778ddb19',\n    1109000: 'b35447f1ad76f95bc4f5886e4028d33acb3ad7b5000dd15516d3f11ce4baa990',\n    1110000: 'ac7baff996062bfaaaddd7d496b17e3ec1c8d34b2143095645ff22fb3888ae00',\n    1111000: '430bbbdcca36b2d69b6a2dd8b07c583a060a467e5f9acbc6de62462e1f7c7036',\n    1112000: 'e5274dea029dc44baff55c05b0555f91b74d29ffd40e3a8c4e2c5b57f9d40bef',\n    1113000: 'cf43863249fa42cfe108220dd40169dac702b0dd9cf5cb699cf2fc96feda8371',\n    1114000: 'fa1c0e551784d21c451564124d2d730e616724f3e535de3c186bcdeb47e80a8f',\n    1115000: '49fe6ecee35a397b83b5a704e950ad028cfb4b7e7a524021e789f4acc0fd6ffe',\n    1116000: '74ecded36751aa8b7901b31f0d16d75d111fc3c40b567f649c04f74ed028aa5c',\n    1117000: 'd9ca760a22190bdf545766b47d963c738a4edcc27f4d15ca801b35751577cfa7',\n    1118000: 'c28d42f871682800ac4e867608227cfb6bc4c00b618e83a8556f201a1c28813c',\n    1119000: 'c5fafc4e1785b0b9e84bb052e392154a5ba1aefe612998017e90772bcd554e08',\n    1120000: 'aa054d428bc9ccee0761da92163817163413065fe1e67ef79a056c5233ea3476',\n    1121000: '0df295bb944218503bd1bf66d2ece0c50fd22dae3391b80673a7ad1e4e5c3934',\n    1122000: 'a13abb350a26673b3933b1de307a60a6845ca594d502599548c6253e21a6d8e8',\n    1123000: 'a4bc6a3abf9ed1f4b14338ff0f03f83456312bc91a93fa89ae6db493050115e1',\n    1124000: '65869938df99adf0dda76200291ce09a54c9bcc787e4bb62cd72c367db58f4f0',\n    1125000: 'ea5e918233b14c3c73d488a906e3741c61bdcafe0393bd0404168fe80c950a46',\n    1126000: 'ce88cd35104fcec51bcee77302e03162dc694802536f5b668786b2245e61bca5',\n    1127000: 'ea19c0c8d205be4be87d02c5301c9ed331e7d75e25b93d1c2137c248882af515',\n    1128000: '006f32d63c2a3adcf4fbad0b0629c97f1beab6446a9c27fbde9472f2d066219e',\n    1129000: '218e5392e1ecf471c3bbc3d79c24dee30ac8db315dbeb61317318efb3f221163',\n    1130000: '30b9da0bd8364e9cd5551b2529341a01a3b7257a238d15b2560e2c99fdb324e8',\n    1131000: '8a7f382cfa023d2eba6639443e67206f8883b57d23ce7e1339234b8bb3098a82',\n    1132000: 'bf9af68a6fe2112d8fe311dfd52334ae2e7b0bac6675c9ebfddb1f386c212668',\n    1133000: '1a30951e2be633502a47c255a93ddbb9ed231d6bb4c55a807c0e910b437766b3',\n    1134000: 'a9bcaf3300b7915e701a8e396eb13f0c7287576323420be7aab3c3ba48020f76',\n    1135000: '337eed9ed072b5ad862af2d3d651f1b49fa852abc590b7e1c2dc381b496f438a',\n    1136000: '208761dbc29ec58302d722a05e937a3cf9e78bfb6495be395dd7b54f02e169dc',\n    1137000: '4e5b67ff3324b64e268049fdc3d82982b847ee359d409ade6368864c38a111e5',\n    1138000: '55d1d0833021a664e85eec8cc90a0985e67cc80d28841aaa8c2231ec28087ebb',\n    1139000: 'e750ada1ec9fa0f2f2461ed68958c7d116a699a82ec12911da5563139f8df19e',\n    1140000: '9cf81407b6ccc8046f0233f97484166945758f7392bb54841c912fcb34cf205c',\n    1141000: 'fccf32b2fae03e3b6b562483776625f9843cd68734c55659e2069cde7e383170',\n    1142000: 'c3608c215dd6569da6c1871c4d72a09ab1caa9663647f2a9454b5693d5d72a65',\n    1143000: 'bd39cb8c4e529d15bbea6baeec66afe52ca18afe32bd812f28fbb0676647cdff',\n    1144000: '6e42d02538565ce7e2d9bf31a304f1fd0ac122d35d17a030160575815901b0b1',\n    1145000: 'b9722e1de2904ce1219140fffb1f4f9f5a041f885faa634404238d103c738b4c',\n    1146000: 'd4de4271459966cee774f538a243d7db0689b213b296463d42e45c93194d7861',\n    1147000: '51fadf109f22bb85574d0fbcbd0b20992983e89aee3d415a7b1c37c44775d9a9',\n    1148000: '137e1fe8da31680d21a42e7421eb608a883a497314e4404625ce44b0edadde6a',\n    1149000: 'cb87867eb04203ce15e0763a2f4389376cea75e0a2877f55e2911c575bef07a8',\n    1150000: '977528ca7953a2c9c19fefaa3aab7ebdec3ac324d74a07d83764ba25d9be0689',\n    1151000: 'a09c51c832600ded63a19201df008075273ea248fd406886e93a2cbaa3bba46b',\n    1152000: '0e5367cfa0f00dd932a5bcc00dcc807fa6825161806bed588e16a57947b4b32d',\n    1153000: '55a9de3dcde2efb56a3c5fea7d22b98c1e180db9a4d4f4f6be7aae1f1cbd7608',\n    1154000: 'abc58cf71c4691ebfaef920252730cf69abbe9de88b424c03051b9b03e85d45a',\n    1155000: '4f074ce73c8a096620b8a32498362eb66a072eae95d561f2d53557cd513ae785',\n    1156000: '540a838a0f0a8834466b17dd456d35b8acae2ec8419f8bd9a704d9ea439062ac',\n    1157000: 'd5310ac671abdb658ea028db86c23fc729af965f91d67a37218c1412cf32a1f5',\n    1158000: '162d906a07e6c35e7c3ebf7069a200521605a97920f5b589d31b19bfd7766ee2',\n    1159000: '600bd8f5e1e62219e220f4dcb650db5812e79956f95ae8a50e83126932685ee0',\n    1160000: '91319398d1a805fac8582c8485e6d84e7490d6cfa6e44e2c630665b6bce0e6b8',\n    1161000: 'f7ad3cff6ee76e1e3df4abe70c600e4af66e1df55bf7b03aee12251d4455a1d4',\n    1162000: '85b9fbba669c2a4d3f85cdb5123f9538c05bd66172b7236d756703f99258454d',\n    1163000: '966085d767d1e5e2e8baf8eda8c11472ec5351181c418b503585284009aaea79',\n    1164000: '1c94e1b531215c019b12caf407296d8868481f49524b7180c7161b0363c1f789',\n    1165000: '803b6bf93735aeae2cf607824e2adf0d754b58da2516c2da1e485c697e472143',\n    1166000: '872561a82f7991633d0927d25cb659d096bbe556fe6dac7a0b6a679820733069',\n    1167000: '6bd7cdd605a3179b54c8af88d1638bf8133fab12cbf0a78d37cf21eddf4395a1',\n    1168000: '79946f5758c1817239cc642d27298bd710983551a8236e49832c6d818b097337',\n    1169000: 'b0994c60728e74de4aa361f37fa85e5296ce3188ae4e0b66d7b34fe86a239c9c',\n    1170000: 'a54188a5a64e0cf8da2406d16a0ac3983b087fc7d6231b6f8abf92cf11dc78cd',\n    1171000: 'ec2924d98e470cc6359821e6468df2c15d60301861d443188730342581230ef2',\n    1172000: 'b4ac11116aa73ce19428009a80e583e19dc9bcd380f7f7ce272a92921d5868d2',\n    1173000: '501d3551f762999dd5a799f3c5658fff2a7f3aff0511488272cd7693fefb8f9d',\n    1174000: '4660074ea48a78ae453cb14b694b2844cc0fb63ed9352ed20d11158bbb5c1f28',\n    1175000: '0727f6b1d9f8fe5677a9ffa0d475f53f5a419ef90b80896c22c2c95de22175de',\n    1176000: '150633d6a35496c24a93c9e19817e90f649c56b7e2558f99e97325bfd5df8b17',\n    1177000: '0849e19f22571b62dba8ff02f6b5a064a7ac36e7ed491321b3663567e8e17294',\n    1178000: '770dd463e7bad80f689f12934e4ae06e24378d1545dcf211fd143beaef49464e',\n    1179000: '059d383dcc60a49b658b674d92fc35cab07b06329c58d73818b6387cb0c06534',\n    1180000: 'e547cb3c636243ca9ae4cfb92c30a0f583eda84e329a5c1e5f64a26fc6fc791e',\n    1181000: '4521a4396ab02f73d45d7a3393ea1c602d255778d52c12079c88bfbad32aab43',\n    1182000: '051cfe993e4b0b34233403a9e8c397dd50e8b78a30fb07e9c260604ee9e624a9',\n    1183000: '44a69c99bb8b85e84ae279f2d8e5400d51cb3d5f0bcd178db49d55548cd66191',\n    1184000: '2a1d23c9bb3c71a533e0c9d25b03bfa7e9db8e014645f3e7fbede6d99fff0191',\n    1185000: 'bb90d6c6d77819163a9e909ee621d874707cdb21c91b1d9e861b204cf37d0ffa',\n    1186000: '4a92051b738ea0e28c64c64f1eb6f0405bc7c3427bef91ff20f4c43cf084d750',\n    1187000: 'f782ac330ca20fb5d8a094ee0f0f8c086a76e3f03ecc6a2c42f8fd07e52e0f41',\n    1188000: '94cb7b653dd3d838c186420158cf0e73db73ec28deaf67d9a2ca902caba4141a',\n    1189000: 'c8128e59b9ec948de890184578a113478ea63f7d57cb75c2c8d5c001a5a724c0',\n    1190000: '4da643bd35e5b98932ae21515a6bffb9c72f2cd8d514cd2d7eac1922af785c3f',\n    1191000: '0f922d86658ac3f53c5f9db360c68ab3f3253a925f23e1323820e3384214719a',\n    1192000: '4c3ab631cf5ba0c236f7c64af6f790fc24448319de6f75dbd28df4e2648d0b7d',\n    1193000: 'eda118d1fac3470a1f8f01f5c78108c8ecdcd6420be30f6d20f1d1831e7b6975',\n    1194000: '5723fff88abd9bb5088476fa5f4221a61c6f8a718703a92f13248ad350abeea2',\n    1195000: '1715846f82d011919e3446c6ce675a65fb80338bd791d4e735702c4767d9adc4',\n    1196000: 'b497667996aee2db61e88f442e728be15ab0b2b64cfd43198691fcf6cdafacc8',\n    1197000: '309a6170d837b8cb334fb888a64ed4e47e6592747e93c8e9d1bf7d608cfef87d',\n    1198000: '3ea918ef64a67dec20051519e6aefaeb7aca2d8583baca9ad5c5bd07073e513a',\n    1199000: '4ec7b7361b0243e5b2996a16e3b27acd662126b95fe542a487c7030e47ea3667',\n    1200000: 'b829c742686fcd642d0f9443336d7e2c4eab81667c90ce553df1350ed10b4233',\n    1201000: '44c022887f1e126fd281b1cae26b2017fa6415a64b105762c87643204ce165a5',\n    1202000: 'b11cc739eb28a14f4e47be125aa7e62d6d6f90c8f8014ee70044ed506d53d938',\n    1203000: '997a7c5fd7a98b39c9ca0790519924d73c3567656b605c97a6fdb7b406c3c64d',\n    1204000: '7d25d872e17195ee277243f7a5a39aa64d8750cec62e4777146acf61a8e76b04',\n    1205000: 'ce8486ae745a4645bee081ef3291d9505174bed05b0668d963b2998b7643dbb0',\n    1206000: '46a0bcea3c411c600dffe3e06e3d1dfbf5879a7ec4dcf3848e794cefcbf2bc0b',\n    1207000: '37e6297bf6e4e2bdd40401d4d7f95e3e3bdafd4a7f76b9c52865cefc6b82b20b',\n    1208000: 'd09e3982a9827b8cf56a5a2f4031dc6b082926c1fd57b63beaaa6cfd534eb902',\n    1209000: '54ae9010a9f146c83464e7ee60b30d9dbee36418561abc4e8d61bce9baa2d21d',\n    1210000: '5dcfd33f8e5ac21c9ba8553758b8cd8afae7961cad428530b5109c2db2ebf39f',\n    1211000: '91c952348bb2c3dfac0d6531a3dac770ea6dab571af257530e9c55493c96bdd9',\n    1212000: 'e62cc3fe044a7f5de4c04a8aed5619548f9d5c6fad9f989d3382cb96de1d780d',\n    1213000: '66b46ffdca8acf1dd04528dadb28b6ac4ce38807c1b84abd685d4ddb3dc59a34',\n    1214000: '2ce4091756ad23746bab4906f46545953cadaf61deae0d78e8a10d4eb51866b1',\n    1215000: '83ce3ca087799cdc4b4c5e7cfeb4a127708724a7ca76aa5f7f4ec1ed48b5fca6',\n    1216000: '7d07b739b7991fbd74926281bf51bba9d5721afab39598720f9ff5f7410a6721',\n    1217000: '76adf49491670d0e8379058eacf0228f330f3c18955dfea1ebe43bc11ee065f3',\n    1218000: '77f422e7301a81692dec69e5c6d35fa988a00a4d820ad0ebb1d595add36558cc',\n    1219000: '8ba9d944f8c468c81799294aeea8dc05ed1bb90bb26552fcd190bd88fedcddf2',\n    1220000: '00330367c255e0fe51b374597995c53353bc5700ad7d603cbd4197141933fe9c',\n    1221000: '3ba8b316b7964f31fdf628ed869a6fd023680cca6611257a31efe22e4d17e578',\n    1222000: '016e58d3fb6a29a3f9281789359460e776e9feb2f0db500482b6e231e1272aef',\n    1223000: 'fdfe767c29a3de7acd913b627d1e5fa887a1af9974f6a8a6474db822468c785c',\n    1224000: '92239f6207bff3689c554e92b24fe2e7be4a2203104ad8ef08b2c6bedd9aeccf',\n    1225000: '9a2f2dd9527b533d3d743efc55236e73e15192171bc8d0cd910918d1ab00aef7',\n    1226000: 'eb8269c75b8c5f66e6ea88ad70883dddcf8a75a45198ca7a46eb0ec606a791bb',\n    1227000: '5c82e624390cd57942dc9d64344eaa3d8991e0437e01802473053245b706290c',\n    1228000: '51e9a7d727f07fc01be7c03e3dd854eb666697f05bf89259baac628520d4402c',\n    1229000: 'c4bfdb651c9abdeda717fb9c8a4c8a6c9c0f78c13d3e6cae3f24f504d734c643',\n    1230000: '9f1ce781d16f2334567cbfb22fff42c14d2b9290cc2883746f435a1fb127021d',\n    1231000: '5c996634b377412ae0a3d8f541f3cc4a354aab72c198aa23a5cfc2678cbabf09',\n    1232000: '86702316a2d1730fbae01a08f36fffe5bf6d3ebb7d76b35a1617713766698b46',\n    1233000: 'fb16b63916c0287cb9b01d0c5aad626ced1b73c49a374c9009703aa90fd27a82',\n    1234000: '7c6f7904602ccd86bfb05cb8d6b5547c989c57cb2e214e93f1220fa4fe29bcb0',\n    1235000: '898b0f20811f52aa5a6bd0c35eff86fca3fbe3b066e423644fa77b2e269d9513',\n    1236000: '39128910ef624b6a8bbd390a311b5587c0991cda834eed996d814fe410cac352',\n    1237000: 'a0709afeedb64af4168ce8cf3dbda667a248df8e91da96acb2333686a2b89325',\n    1238000: 'e00075e7ba8c18cc277bfc5115ae6ff6b9678e6e99efd6e45f549ef8a3981a3d',\n    1239000: '3fba891600738f2d37e279209d52bbe6dc7ce005eeed62048247c96f370e7cd5',\n    1240000: 'def9bf1bec9325db90bb070f532972cfdd74e814c2b5e74a4d5a7c09a963a5f1',\n    1241000: '6a5d187e32bc189ac786959e1fe846031b97ae1ce202c22e1bdb1d2a963005fd',\n    1242000: 'a74d7c0b104eaf76c53a3a31ce51b75bbd8e05b5e84c31f593f505a13d83634c',\n}\n"
  },
  {
    "path": "lbry/wallet/claim_proofs.py",
    "content": "import struct\nimport binascii\nfrom lbry.crypto.hash import double_sha256\n\n\nclass InvalidProofError(Exception):\n    pass\n\n\ndef get_hash_for_outpoint(txhash, nout, height_of_last_takeover):\n    return double_sha256(\n        double_sha256(txhash) +\n        double_sha256(str(nout).encode()) +\n        double_sha256(struct.pack('>Q', height_of_last_takeover))\n    )\n\n\n# noinspection PyPep8\ndef verify_proof(proof, root_hash, name):\n    previous_computed_hash = None\n    reverse_computed_name = ''\n    verified_value = False\n    for i, node in enumerate(proof['nodes'][::-1]):\n        found_child_in_chain = False\n        to_hash = b''\n        previous_child_character = None\n        for child in node['children']:\n            if child['character'] < 0 or child['character'] > 255:\n                raise InvalidProofError(\"child character not int between 0 and 255\")\n            if previous_child_character:\n                if previous_child_character >= child['character']:\n                    raise InvalidProofError(\"children not in increasing order\")\n            previous_child_character = child['character']\n            to_hash += bytes((child['character'],))\n            if 'nodeHash' in child:\n                if len(child['nodeHash']) != 64:\n                    raise InvalidProofError(\"invalid child nodeHash\")\n                to_hash += binascii.unhexlify(child['nodeHash'])[::-1]\n            else:\n                if previous_computed_hash is None:\n                    raise InvalidProofError(\"previous computed hash is None\")\n                if found_child_in_chain is True:\n                    raise InvalidProofError(\"already found the next child in the chain\")\n                found_child_in_chain = True\n                reverse_computed_name += chr(child['character'])\n                to_hash += previous_computed_hash\n\n        if not found_child_in_chain:\n            if i != 0:\n                raise InvalidProofError(\"did not find the alleged child\")\n        if i == 0 and 'txhash' in proof and 'nOut' in proof and 'last takeover height' in proof:\n            if len(proof['txhash']) != 64:\n                raise InvalidProofError(f\"txhash was invalid: {proof['txhash']}\")\n            if not isinstance(proof['nOut'], int):\n                raise InvalidProofError(f\"nOut was invalid: {proof['nOut']}\")\n            if not isinstance(proof['last takeover height'], int):\n                raise InvalidProofError(\n                    f\"last takeover height was invalid: {proof['last takeover height']}\")\n            to_hash += get_hash_for_outpoint(\n                binascii.unhexlify(proof['txhash'])[::-1],\n                proof['nOut'],\n                proof['last takeover height']\n            )\n            verified_value = True\n        elif 'valueHash' in node:\n            if len(node['valueHash']) != 64:\n                raise InvalidProofError(\"valueHash was invalid\")\n            to_hash += binascii.unhexlify(node['valueHash'])[::-1]\n\n        previous_computed_hash = double_sha256(to_hash)\n\n    if previous_computed_hash != binascii.unhexlify(root_hash)[::-1]:\n        raise InvalidProofError(\"computed hash does not match roothash\")\n    if 'txhash' in proof and 'nOut' in proof:\n        if not verified_value:\n            raise InvalidProofError(\"mismatch between proof claim and outcome\")\n    target = reverse_computed_name[::-1].encode('ISO-8859-1').decode()\n    if 'txhash' in proof and 'nOut' in proof:\n        if name != target:\n            raise InvalidProofError(\"name did not match proof\")\n    if not name.startswith(target):\n        raise InvalidProofError(\"name fragment does not match proof\")\n    return True\n"
  },
  {
    "path": "lbry/wallet/coinselection.py",
    "content": "from random import Random\nfrom typing import List\n\nfrom lbry.wallet.transaction import OutputEffectiveAmountEstimator\n\nMAXIMUM_TRIES = 100000\n\nSTRATEGIES = ['sqlite']  # sqlite coin chooser is in database.py\n\n\ndef strategy(method):\n    STRATEGIES.append(method.__name__)\n    return method\n\n\nclass CoinSelector:\n\n    def __init__(self, target: int, cost_of_change: int, seed: str = None) -> None:\n        self.target = target\n        self.cost_of_change = cost_of_change\n        self.exact_match = False\n        self.tries = 0\n        self.random = Random(seed)\n        if seed is not None:\n            self.random.seed(seed, version=1)\n\n    def select(\n            self, txos: List[OutputEffectiveAmountEstimator],\n            strategy_name: str = None) -> List[OutputEffectiveAmountEstimator]:\n        if not txos:\n            return []\n        available = sum(c.effective_amount for c in txos)\n        if self.target > available:\n            return []\n        return getattr(self, strategy_name or \"standard\")(txos, available)\n\n    @strategy\n    def prefer_confirmed(self, txos: List[OutputEffectiveAmountEstimator],\n                         available: int) -> List[OutputEffectiveAmountEstimator]:\n        return (\n            self.only_confirmed(txos, available) or\n            self.standard(txos, available)\n        )\n\n    @strategy\n    def only_confirmed(self, txos: List[OutputEffectiveAmountEstimator],\n                       _) -> List[OutputEffectiveAmountEstimator]:\n        confirmed = [t for t in txos if t.txo.tx_ref and t.txo.tx_ref.height > 0]\n        if not confirmed:\n            return []\n        confirmed_available = sum(c.effective_amount for c in confirmed)\n        if self.target > confirmed_available:\n            return []\n        return self.standard(confirmed, confirmed_available)\n\n    @strategy\n    def standard(self, txos: List[OutputEffectiveAmountEstimator],\n                 available: int) -> List[OutputEffectiveAmountEstimator]:\n        return (\n            self.branch_and_bound(txos, available) or\n            self.closest_match(txos, available) or\n            self.random_draw(txos, available)\n        )\n\n    @strategy\n    def branch_and_bound(self, txos: List[OutputEffectiveAmountEstimator],\n                         available: int) -> List[OutputEffectiveAmountEstimator]:\n        # see bitcoin implementation for more info:\n        # https://github.com/bitcoin/bitcoin/blob/master/src/wallet/coinselection.cpp\n\n        txos.sort(reverse=True)\n\n        current_value = 0\n        current_available_value = available\n        current_selection: List[bool] = []\n        best_waste = self.cost_of_change\n        best_selection: List[bool] = []\n\n        while self.tries < MAXIMUM_TRIES:\n            self.tries += 1\n\n            backtrack = False\n            if current_value + current_available_value < self.target or \\\n               current_value > self.target + self.cost_of_change:\n                backtrack = True\n            elif current_value >= self.target:\n                new_waste = current_value - self.target\n                if new_waste <= best_waste:\n                    best_waste = new_waste\n                    best_selection = current_selection[:]\n                backtrack = True\n\n            if backtrack:\n                while current_selection and not current_selection[-1]:\n                    current_selection.pop()\n                    current_available_value += txos[len(current_selection)].effective_amount\n\n                if not current_selection:\n                    break\n\n                current_selection[-1] = False\n                utxo = txos[len(current_selection) - 1]\n                current_value -= utxo.effective_amount\n\n            else:\n                utxo = txos[len(current_selection)]\n                current_available_value -= utxo.effective_amount\n                previous_utxo = txos[len(current_selection) - 1] if current_selection else None\n                if current_selection and not current_selection[-1] and previous_utxo and \\\n                   utxo.effective_amount == previous_utxo.effective_amount and \\\n                   utxo.fee == previous_utxo.fee:\n                    current_selection.append(False)\n                else:\n                    current_selection.append(True)\n                    current_value += utxo.effective_amount\n\n        if best_selection:\n            self.exact_match = True\n            return [\n                txos[i] for i, include in enumerate(best_selection) if include\n            ]\n\n        return []\n\n    @strategy\n    def closest_match(self, txos: List[OutputEffectiveAmountEstimator],\n                      _) -> List[OutputEffectiveAmountEstimator]:\n        \"\"\" Pick one UTXOs that is larger than the target but with the smallest change. \"\"\"\n        target = self.target + self.cost_of_change\n        smallest_change = None\n        best_match = None\n        for txo in txos:\n            if txo.effective_amount >= target:\n                change = txo.effective_amount - target\n                if smallest_change is None or change < smallest_change:\n                    smallest_change, best_match = change, txo\n        return [best_match] if best_match else []\n\n    @strategy\n    def random_draw(self, txos: List[OutputEffectiveAmountEstimator],\n                    _) -> List[OutputEffectiveAmountEstimator]:\n        \"\"\" Accumulate UTXOs at random until there is enough to cover the target. \"\"\"\n        target = self.target + self.cost_of_change\n        self.random.shuffle(txos, random=self.random.random)  # pylint: disable=deprecated-argument\n        selection = []\n        amount = 0\n        for coin in txos:\n            selection.append(coin)\n            amount += coin.effective_amount\n            if amount >= target:\n                return selection\n        return []\n"
  },
  {
    "path": "lbry/wallet/constants.py",
    "content": "NULL_HASH32 = b'\\x00'*32\n\nCENT = 1000000\nCOIN = 100*CENT\nDUST = 1000\n\nTIMEOUT = 30.0\n\nTXO_TYPES = {\n    \"other\": 0,\n    \"stream\": 1,\n    \"channel\": 2,\n    \"support\": 3,\n    \"purchase\": 4,\n    \"collection\": 5,\n    \"repost\": 6,\n}\n\nCLAIM_TYPE_NAMES = [\n    'stream',\n    'channel',\n    'collection',\n    'repost',\n]\n\nCLAIM_TYPES = [\n    TXO_TYPES[name] for name in CLAIM_TYPE_NAMES\n]\n"
  },
  {
    "path": "lbry/wallet/database.py",
    "content": "import os\nimport logging\nimport asyncio\nimport sqlite3\nimport platform\nfrom binascii import hexlify\nfrom collections import defaultdict\nfrom dataclasses import dataclass\nfrom contextvars import ContextVar\nfrom typing import Tuple, List, Union, Callable, Any, Awaitable, Iterable, Dict, Optional\nfrom datetime import date\n\nfrom prometheus_client import Gauge, Counter, Histogram\nfrom lbry.utils import LockWithMetrics\n\nfrom .bip32 import PublicKey\nfrom .transaction import Transaction, Output, OutputScript, TXRefImmutable, Input\nfrom .constants import TXO_TYPES, CLAIM_TYPES\nfrom .util import date_to_julian_day\n\nfrom concurrent.futures.thread import ThreadPoolExecutor  # pylint: disable=wrong-import-order\nif platform.system() == 'Windows' or ({'ANDROID_ARGUMENT', 'KIVY_BUILD'} & os.environ.keys()):\n    from concurrent.futures.thread import ThreadPoolExecutor as ReaderExecutorClass  # pylint: disable=reimported\nelse:\n    from concurrent.futures.process import ProcessPoolExecutor as ReaderExecutorClass\n\n\nlog = logging.getLogger(__name__)\nsqlite3.enable_callback_tracebacks(True)\n\nHISTOGRAM_BUCKETS = (\n    .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')\n)\n\n\n@dataclass\nclass ReaderProcessState:\n    cursor: sqlite3.Cursor\n\n\nreader_context: Optional[ContextVar[ReaderProcessState]] = ContextVar('reader_context')\n\n\ndef initializer(path):\n    db = sqlite3.connect(path)\n    db.row_factory = dict_row_factory\n    db.executescript(\"pragma journal_mode=WAL;\")\n    reader = ReaderProcessState(db.cursor())\n    reader_context.set(reader)\n\n\ndef run_read_only_fetchall(sql, params):\n    cursor = reader_context.get().cursor\n    try:\n        return cursor.execute(sql, params).fetchall()\n    except (Exception, OSError) as e:\n        log.exception('Error running transaction:', exc_info=e)\n        raise\n\n\ndef run_read_only_fetchone(sql, params):\n    cursor = reader_context.get().cursor\n    try:\n        return cursor.execute(sql, params).fetchone()\n    except (Exception, OSError) as e:\n        log.exception('Error running transaction:', exc_info=e)\n        raise\n\n\nclass AIOSQLite:\n    reader_executor: ReaderExecutorClass\n\n    waiting_writes_metric = Gauge(\n        \"waiting_writes_count\", \"Number of waiting db writes\", namespace=\"daemon_database\"\n    )\n    waiting_reads_metric = Gauge(\n        \"waiting_reads_count\", \"Number of waiting db writes\", namespace=\"daemon_database\"\n    )\n    write_count_metric = Counter(\n        \"write_count\", \"Number of database writes\", namespace=\"daemon_database\"\n    )\n    read_count_metric = Counter(\n        \"read_count\", \"Number of database reads\", namespace=\"daemon_database\"\n    )\n    acquire_write_lock_metric = Histogram(\n        'write_lock_acquired', 'Time to acquire the write lock', namespace=\"daemon_database\", buckets=HISTOGRAM_BUCKETS\n    )\n    held_write_lock_metric = Histogram(\n        'write_lock_held', 'Length of time the write lock is held for', namespace=\"daemon_database\",\n        buckets=HISTOGRAM_BUCKETS\n    )\n\n    def __init__(self):\n        # has to be single threaded as there is no mapping of thread:connection\n        self.writer_executor = ThreadPoolExecutor(max_workers=1)\n        self.writer_connection: Optional[sqlite3.Connection] = None\n        self._closing = False\n        self.query_count = 0\n        self.write_lock = LockWithMetrics(self.acquire_write_lock_metric, self.held_write_lock_metric)\n        self.writers = 0\n        self.read_ready = asyncio.Event()\n        self.urgent_read_done = asyncio.Event()\n\n    @classmethod\n    async def connect(cls, path: Union[bytes, str], *args, **kwargs):\n        sqlite3.enable_callback_tracebacks(True)\n        db = cls()\n\n        def _connect_writer():\n            db.writer_connection = sqlite3.connect(path, *args, **kwargs)\n\n        readers = max(os.cpu_count() - 2, 2)\n        db.reader_executor = ReaderExecutorClass(\n            max_workers=readers, initializer=initializer, initargs=(path, )\n        )\n        await asyncio.get_event_loop().run_in_executor(db.writer_executor, _connect_writer)\n        db.read_ready.set()\n        db.urgent_read_done.set()\n        return db\n\n    async def close(self):\n        if self._closing:\n            return\n        self._closing = True\n\n        def __checkpoint_and_close(conn: sqlite3.Connection):\n            conn.execute(\"PRAGMA WAL_CHECKPOINT(FULL);\")\n            log.info(\"DB checkpoint finished.\")\n            conn.close()\n        await asyncio.get_event_loop().run_in_executor(\n            self.writer_executor, __checkpoint_and_close, self.writer_connection)\n        self.writer_executor.shutdown(wait=True)\n        self.reader_executor.shutdown(wait=True)\n        self.read_ready.clear()\n        self.writer_connection = None\n\n    def executemany(self, sql: str, params: Iterable):\n        params = params if params is not None else []\n        # this fetchall is needed to prevent SQLITE_MISUSE\n        return self.run(lambda conn: conn.executemany(sql, params).fetchall())\n\n    def executescript(self, script: str) -> Awaitable:\n        return self.run(lambda conn: conn.executescript(script))\n\n    async def _execute_fetch(self, sql: str, parameters: Iterable = None,\n                             read_only=False, fetch_all: bool = False) -> List[dict]:\n        read_only_fn = run_read_only_fetchall if fetch_all else run_read_only_fetchone\n        parameters = parameters if parameters is not None else []\n        still_waiting = False\n        urgent_read = False\n        if read_only:\n            self.waiting_reads_metric.inc()\n            self.read_count_metric.inc()\n            try:\n                while self.writers and not self._closing:  # more writes can come in while we are waiting for the first\n                    if not urgent_read and still_waiting and self.urgent_read_done.is_set():\n                        #  throttle the writes if they pile up\n                        self.urgent_read_done.clear()\n                        urgent_read = True\n                    #  wait until the running writes have finished\n                    await self.read_ready.wait()\n                    still_waiting = True\n                if self._closing:\n                    raise asyncio.CancelledError()\n                return await asyncio.get_event_loop().run_in_executor(\n                    self.reader_executor, read_only_fn, sql, parameters\n                )\n            finally:\n                if urgent_read:\n                    #  unthrottle the writers if they had to be throttled\n                    self.urgent_read_done.set()\n                self.waiting_reads_metric.dec()\n        if fetch_all:\n            return await self.run(lambda conn: conn.execute(sql, parameters).fetchall())\n        return await self.run(lambda conn: conn.execute(sql, parameters).fetchone())\n\n    async def execute_fetchall(self, sql: str, parameters: Iterable = None,\n                               read_only=False) -> List[dict]:\n        return await self._execute_fetch(sql, parameters, read_only, fetch_all=True)\n\n    async def execute_fetchone(self, sql: str, parameters: Iterable = None,\n                               read_only=False) -> List[dict]:\n        return await self._execute_fetch(sql, parameters, read_only, fetch_all=False)\n\n    def execute(self, sql: str, parameters: Iterable = None) -> Awaitable[sqlite3.Cursor]:\n        parameters = parameters if parameters is not None else []\n        return self.run(lambda conn: conn.execute(sql, parameters))\n\n    async def run(self, fun, *args, **kwargs):\n        self.write_count_metric.inc()\n        self.waiting_writes_metric.inc()\n        # it's possible many writes are coming in one after the other, these can\n        # block reader calls for a long time\n        # if the reader waits for the writers to finish and then has to wait for\n        # yet more, it will clear the urgent_read_done event to block more writers\n        # piling on\n        try:\n            await self.urgent_read_done.wait()\n        except Exception as e:\n            self.waiting_writes_metric.dec()\n            raise e\n        self.writers += 1\n        # block readers\n        self.read_ready.clear()\n        try:\n            async with self.write_lock:\n                if self._closing:\n                    raise asyncio.CancelledError()\n                return await asyncio.get_event_loop().run_in_executor(\n                    self.writer_executor, lambda: self.__run_transaction(fun, *args, **kwargs)\n                )\n        finally:\n            self.writers -= 1\n            self.waiting_writes_metric.dec()\n            if not self.writers:\n                # unblock the readers once the last enqueued writer finishes\n                self.read_ready.set()\n\n    def __run_transaction(self, fun: Callable[[sqlite3.Connection, Any, Any], Any], *args, **kwargs):\n        self.writer_connection.execute('begin')\n        try:\n            self.query_count += 1\n            result = fun(self.writer_connection, *args, **kwargs)  # type: ignore\n            self.writer_connection.commit()\n            return result\n        except (Exception, OSError) as e:\n            log.exception('Error running transaction:', exc_info=e)\n            self.writer_connection.rollback()\n            log.warning(\"rolled back\")\n            raise\n\n    async def run_with_foreign_keys_disabled(self, fun, *args, **kwargs):\n        self.write_count_metric.inc()\n        self.waiting_writes_metric.inc()\n        try:\n            await self.urgent_read_done.wait()\n        except Exception as e:\n            self.waiting_writes_metric.dec()\n            raise e\n        self.writers += 1\n        self.read_ready.clear()\n        try:\n            async with self.write_lock:\n                if self._closing:\n                    raise asyncio.CancelledError()\n                return await asyncio.get_event_loop().run_in_executor(\n                    self.writer_executor, self.__run_transaction_with_foreign_keys_disabled, fun, args, kwargs\n                )\n        finally:\n            self.writers -= 1\n            self.waiting_writes_metric.dec()\n            if not self.writers:\n                self.read_ready.set()\n\n    def __run_transaction_with_foreign_keys_disabled(self,\n                                                     fun: Callable[[sqlite3.Connection, Any, Any], Any],\n                                                     args, kwargs):\n        foreign_keys_enabled, = self.writer_connection.execute(\"pragma foreign_keys\").fetchone()\n        if not foreign_keys_enabled:\n            raise sqlite3.IntegrityError(\"foreign keys are disabled, use `AIOSQLite.run` instead\")\n        try:\n            self.writer_connection.execute('pragma foreign_keys=off').fetchone()\n            return self.__run_transaction(fun, *args, **kwargs)\n        finally:\n            self.writer_connection.execute('pragma foreign_keys=on').fetchone()\n\n\ndef constraints_to_sql(constraints, joiner=' AND ', prepend_key=''):\n    sql, values = [], {}\n    for key, constraint in constraints.items():\n        tag = '0'\n        if '#' in key:\n            key, tag = key[:key.index('#')], key[key.index('#')+1:]\n        col, op, key = key, '=', key.replace('.', '_')\n        if not key:\n            sql.append(constraint)\n            continue\n        if key.startswith('$$'):\n            col, key = col[2:], key[1:]\n        elif key.startswith('$'):\n            values[key] = constraint\n            continue\n        if key.endswith('__not'):\n            col, op = col[:-len('__not')], '!='\n        elif key.endswith('__is_null'):\n            col = col[:-len('__is_null')]\n            sql.append(f'{col} IS NULL')\n            continue\n        if key.endswith('__is_not_null'):\n            col = col[:-len('__is_not_null')]\n            sql.append(f'{col} IS NOT NULL')\n            continue\n        if key.endswith('__lt'):\n            col, op = col[:-len('__lt')], '<'\n        elif key.endswith('__lte'):\n            col, op = col[:-len('__lte')], '<='\n        elif key.endswith('__gt'):\n            col, op = col[:-len('__gt')], '>'\n        elif key.endswith('__gte'):\n            col, op = col[:-len('__gte')], '>='\n        elif key.endswith('__like'):\n            col, op = col[:-len('__like')], 'LIKE'\n        elif key.endswith('__not_like'):\n            col, op = col[:-len('__not_like')], 'NOT LIKE'\n        elif key.endswith('__in') or key.endswith('__not_in'):\n            if key.endswith('__in'):\n                col, op, one_val_op = col[:-len('__in')], 'IN', '='\n            else:\n                col, op, one_val_op = col[:-len('__not_in')], 'NOT IN', '!='\n            if constraint:\n                if isinstance(constraint, (list, set, tuple)):\n                    if len(constraint) == 1:\n                        values[f'{key}{tag}'] = next(iter(constraint))\n                        sql.append(f'{col} {one_val_op} :{key}{tag}')\n                    else:\n                        keys = []\n                        for i, val in enumerate(constraint):\n                            keys.append(f':{key}{tag}_{i}')\n                            values[f'{key}{tag}_{i}'] = val\n                        sql.append(f'{col} {op} ({\", \".join(keys)})')\n                elif isinstance(constraint, str):\n                    sql.append(f'{col} {op} ({constraint})')\n                else:\n                    raise ValueError(f\"{col} requires a list, set or string as constraint value.\")\n            continue\n        elif key.endswith('__any') or key.endswith('__or'):\n            where, subvalues = constraints_to_sql(constraint, ' OR ', key+tag+'_')\n            sql.append(f'({where})')\n            values.update(subvalues)\n            continue\n        if key.endswith('__and'):\n            where, subvalues = constraints_to_sql(constraint, ' AND ', key+tag+'_')\n            sql.append(f'({where})')\n            values.update(subvalues)\n            continue\n        sql.append(f'{col} {op} :{prepend_key}{key}{tag}')\n        values[prepend_key+key+tag] = constraint\n    return joiner.join(sql) if sql else '', values\n\n\ndef query(select, **constraints) -> Tuple[str, Dict[str, Any]]:\n    sql = [select]\n    limit = constraints.pop('limit', None)\n    offset = constraints.pop('offset', None)\n    order_by = constraints.pop('order_by', None)\n    group_by = constraints.pop('group_by', None)\n\n    accounts = constraints.pop('accounts', [])\n    if accounts:\n        constraints['account__in'] = [a.public_key.address for a in accounts]\n\n    where, values = constraints_to_sql(constraints)\n    if where:\n        sql.append('WHERE')\n        sql.append(where)\n\n    if group_by is not None:\n        sql.append(f'GROUP BY {group_by}')\n\n    if order_by:\n        sql.append('ORDER BY')\n        if isinstance(order_by, str):\n            sql.append(order_by)\n        elif isinstance(order_by, list):\n            sql.append(', '.join(order_by))\n        else:\n            raise ValueError(\"order_by must be string or list\")\n\n    if limit is not None:\n        sql.append(f'LIMIT {limit}')\n\n    if offset is not None:\n        sql.append(f'OFFSET {offset}')\n\n    return ' '.join(sql), values\n\n\ndef interpolate(sql, values):\n    for k in sorted(values.keys(), reverse=True):\n        value = values[k]\n        if isinstance(value, bytes):\n            value = f\"X'{hexlify(value).decode()}'\"\n        elif isinstance(value, str):\n            value = f\"'{value}'\"\n        else:\n            value = str(value)\n        sql = sql.replace(f\":{k}\", value)\n    return sql\n\n\ndef constrain_single_or_list(constraints, column, value, convert=lambda x: x, negate=False):\n    if value is not None:\n        if isinstance(value, list):\n            value = [convert(v) for v in value]\n            if len(value) == 1:\n                if negate:\n                    constraints[f\"{column}__or\"] = {\n                        f\"{column}__is_null\": True,\n                        f\"{column}__not\": value[0]\n                    }\n                else:\n                    constraints[column] = value[0]\n            elif len(value) > 1:\n                if negate:\n                    constraints[f\"{column}__or\"] = {\n                        f\"{column}__is_null\": True,\n                        f\"{column}__not_in\": value\n                    }\n                else:\n                    constraints[f\"{column}__in\"] = value\n        elif negate:\n            constraints[f\"{column}__or\"] = {\n                f\"{column}__is_null\": True,\n                f\"{column}__not\": convert(value)\n            }\n        else:\n            constraints[column] = convert(value)\n    return constraints\n\n\nclass SQLiteMixin:\n\n    SCHEMA_VERSION: Optional[str] = None\n    CREATE_TABLES_QUERY: str\n    MAX_QUERY_VARIABLES = 900\n\n    CREATE_VERSION_TABLE = \"\"\"\n        create table if not exists version (\n            version text\n        );\n    \"\"\"\n\n    def __init__(self, path):\n        self._db_path = path\n        self.db: AIOSQLite = None\n        self.ledger = None\n\n    async def open(self):\n        log.info(\"connecting to database: %s\", self._db_path)\n        self.db = await AIOSQLite.connect(self._db_path, isolation_level=None)\n        if self.SCHEMA_VERSION:\n            tables = [t[0] for t in await self.db.execute_fetchall(\n                \"SELECT name FROM sqlite_master WHERE type='table';\"\n            )]\n            if tables:\n                if 'version' in tables:\n                    version = await self.db.execute_fetchone(\"SELECT version FROM version LIMIT 1;\")\n                    if version == (self.SCHEMA_VERSION,):\n                        return\n                    if version == (\"1.5\",) and self.SCHEMA_VERSION == \"1.6\":\n                        await self.db.execute(\"ALTER TABLE txo ADD COLUMN has_source bool DEFAULT 1;\")\n                        await self.db.execute(\"UPDATE version SET version = ?\", (self.SCHEMA_VERSION,))\n                        return\n                await self.db.executescript('\\n'.join(\n                    f\"DROP TABLE {table};\" for table in tables\n                ) + '\\n' + 'PRAGMA WAL_CHECKPOINT(FULL);' + '\\n' + 'VACUUM;')\n            await self.db.execute(self.CREATE_VERSION_TABLE)\n            await self.db.execute(\"INSERT INTO version VALUES (?)\", (self.SCHEMA_VERSION,))\n        await self.db.executescript(self.CREATE_TABLES_QUERY)\n\n    async def close(self):\n        await self.db.close()\n\n    @staticmethod\n    def _insert_sql(table: str, data: dict, ignore_duplicate: bool = False,\n                    replace: bool = False) -> Tuple[str, List]:\n        columns, values = [], []\n        for column, value in data.items():\n            columns.append(column)\n            values.append(value)\n        policy = \"\"\n        if ignore_duplicate:\n            policy = \" OR IGNORE\"\n        if replace:\n            policy = \" OR REPLACE\"\n        sql = \"INSERT{} INTO {} ({}) VALUES ({})\".format(\n            policy, table, ', '.join(columns), ', '.join(['?'] * len(values))\n        )\n        return sql, values\n\n    @staticmethod\n    def _update_sql(table: str, data: dict, where: str,\n                    constraints: Union[list, tuple]) -> Tuple[str, list]:\n        columns, values = [], []\n        for column, value in data.items():\n            columns.append(f\"{column} = ?\")\n            values.append(value)\n        values.extend(constraints)\n        sql = \"UPDATE {} SET {} WHERE {}\".format(\n            table, ', '.join(columns), where\n        )\n        return sql, values\n\n\ndef dict_row_factory(cursor, row):\n    d = {}\n    for idx, col in enumerate(cursor.description):\n        d[col[0]] = row[idx]\n    return d\n\n\nSQLITE_MAX_INTEGER = 9223372036854775807\n\n\ndef _get_spendable_utxos(transaction: sqlite3.Connection, accounts: List, decoded_transactions: Dict[str, Transaction],\n                         result: Dict[Tuple[bytes, int, bool], List[int]], reserved: List[Transaction],\n                         amount_to_reserve: int, reserved_amount: int, floor: int, ceiling: int,\n                         fee_per_byte: int) -> int:\n    accounts_fmt = \",\".join([\"?\"] * len(accounts))\n    txo_query = \"\"\"\n        SELECT tx.txid, txo.txoid, tx.raw, tx.height, txo.position as nout, tx.is_verified, txo.amount FROM txo\n        INNER JOIN account_address USING (address)\n        LEFT JOIN txi USING (txoid)\n        INNER JOIN tx USING (txid)\n        WHERE txo.txo_type=0 AND txi.txoid IS NULL AND tx.txid IS NOT NULL AND NOT txo.is_reserved\n        AND txo.amount >= ? AND txo.amount < ?\n    \"\"\"\n    if accounts:\n        txo_query += f\"\"\"\n            AND account_address.account {'= ?' if len(accounts_fmt) == 1 else 'IN (' + accounts_fmt + ')'}\n        \"\"\"\n    txo_query += \"\"\"\n        ORDER BY txo.amount ASC, tx.height DESC\n    \"\"\"\n    # prefer confirmed, but save unconfirmed utxos from this selection in case they are needed\n    unconfirmed = []\n    for row in transaction.execute(txo_query, (floor, ceiling, *accounts)):\n        (txid, txoid, raw, height, nout, verified, amount) = row.values()\n        # verified or non verified transactions were found- reset the gap count\n        # multiple txos can come from the same tx, only decode it once and cache\n        if txid not in decoded_transactions:\n            # cache the decoded transaction\n            decoded_transactions[txid] = Transaction(raw)\n        decoded_tx = decoded_transactions[txid]\n        # save the unconfirmed txo for possible use later, if still needed\n        if verified:\n            # add the txo to the reservation, minus the fee for including it\n            reserved_amount += amount\n            reserved_amount -= Input.spend(decoded_tx.outputs[nout]).size * fee_per_byte\n            # mark it as reserved\n            result[(raw, height, verified)].append(nout)\n            reserved.append(txoid)\n            # if we've reserved enough, return\n            if reserved_amount >= amount_to_reserve:\n                return reserved_amount\n        else:\n            unconfirmed.append((txid, txoid, raw, height, nout, verified, amount))\n    # we're popping the items, so to get them in the order they were seen they are reversed\n    unconfirmed.reverse()\n    # add available unconfirmed txos if any were previously found\n    while unconfirmed and reserved_amount < amount_to_reserve:\n        (txid, txoid, raw, height, nout, verified, amount) = unconfirmed.pop()\n        # it's already decoded\n        decoded_tx = decoded_transactions[txid]\n        # add to the reserved amount\n        reserved_amount += amount\n        reserved_amount -= Input.spend(decoded_tx.outputs[nout]).size * fee_per_byte\n        result[(raw, height, verified)].append(nout)\n        reserved.append(txoid)\n    return reserved_amount\n\n\ndef get_and_reserve_spendable_utxos(transaction: sqlite3.Connection, accounts: List, amount_to_reserve: int, floor: int,\n                                    fee_per_byte: int, set_reserved: bool, return_insufficient_funds: bool,\n                                    base_multiplier: int = 100):\n    txs = defaultdict(list)\n    decoded_transactions = {}\n    reserved = []\n\n    reserved_dewies = 0\n    multiplier = base_multiplier\n    gap_count = 0\n\n    while reserved_dewies < amount_to_reserve and gap_count < 5 and floor * multiplier < SQLITE_MAX_INTEGER:\n        previous_reserved_dewies = reserved_dewies\n        reserved_dewies = _get_spendable_utxos(\n            transaction, accounts, decoded_transactions, txs, reserved, amount_to_reserve, reserved_dewies,\n            floor, floor * multiplier, fee_per_byte\n        )\n        floor *= multiplier\n        if previous_reserved_dewies == reserved_dewies:\n            gap_count += 1\n            multiplier **= 2\n        else:\n            gap_count = 0\n            multiplier = base_multiplier\n\n    # reserve the accumulated txos if enough were found\n    if reserved_dewies >= amount_to_reserve:\n        if set_reserved:\n            transaction.executemany(\"UPDATE txo SET is_reserved = ? WHERE txoid = ?\",\n                                    [(True, txoid) for txoid in reserved]).fetchall()\n        return txs\n    # return_insufficient_funds and set_reserved are used for testing\n    return txs if return_insufficient_funds else {}\n\n\nclass Database(SQLiteMixin):\n\n    SCHEMA_VERSION = \"1.6\"\n\n    PRAGMAS = \"\"\"\n        pragma journal_mode=WAL;\n    \"\"\"\n\n    CREATE_ACCOUNT_TABLE = \"\"\"\n        create table if not exists account_address (\n            account text not null,\n            address text not null,\n            chain integer not null,\n            pubkey blob not null,\n            chain_code blob not null,\n            n integer not null,\n            depth integer not null,\n            primary key (account, address)\n        );\n        create index if not exists address_account_idx on account_address (address, account);\n    \"\"\"\n\n    CREATE_PUBKEY_ADDRESS_TABLE = \"\"\"\n        create table if not exists pubkey_address (\n            address text primary key,\n            history text,\n            used_times integer not null default 0\n        );\n    \"\"\"\n\n    CREATE_TX_TABLE = \"\"\"\n        create table if not exists tx (\n            txid text primary key,\n            raw blob not null,\n            height integer not null,\n            position integer not null,\n            is_verified boolean not null default 0,\n            purchased_claim_id text,\n            day integer\n        );\n        create index if not exists tx_purchased_claim_id_idx on tx (purchased_claim_id);\n    \"\"\"\n\n    CREATE_TXO_TABLE = \"\"\"\n        create table if not exists txo (\n            txid text references tx,\n            txoid text primary key,\n            address text references pubkey_address,\n            position integer not null,\n            amount integer not null,\n            script blob not null,\n            is_reserved boolean not null default 0,\n\n            txo_type integer not null default 0,\n            claim_id text,\n            claim_name text,\n            has_source bool,\n\n            channel_id text,\n            reposted_claim_id text\n        );\n        create index if not exists txo_txid_idx on txo (txid);\n        create index if not exists txo_address_idx on txo (address);\n        create index if not exists txo_claim_id_idx on txo (claim_id, txo_type);\n        create index if not exists txo_claim_name_idx on txo (claim_name);\n        create index if not exists txo_txo_type_idx on txo (txo_type);\n        create index if not exists txo_channel_id_idx on txo (channel_id);\n        create index if not exists txo_reposted_claim_idx on txo (reposted_claim_id);\n    \"\"\"\n\n    CREATE_TXI_TABLE = \"\"\"\n        create table if not exists txi (\n            txid text references tx,\n            txoid text references txo primary key,\n            address text references pubkey_address,\n            position integer not null\n        );\n        create index if not exists txi_address_idx on txi (address);\n        create index if not exists first_input_idx on txi (txid, address) where position=0;\n    \"\"\"\n\n    CREATE_TABLES_QUERY = (\n        PRAGMAS +\n        CREATE_ACCOUNT_TABLE +\n        CREATE_PUBKEY_ADDRESS_TABLE +\n        CREATE_TX_TABLE +\n        CREATE_TXO_TABLE +\n        CREATE_TXI_TABLE\n    )\n\n    async def open(self):\n        await super().open()\n        self.db.writer_connection.row_factory = dict_row_factory\n\n    def txo_to_row(self, tx, txo):\n        row = {\n            'txid': tx.id,\n            'txoid': txo.id,\n            'address': txo.get_address(self.ledger),\n            'position': txo.position,\n            'amount': txo.amount,\n            'script': sqlite3.Binary(txo.script.source),\n            'has_source': False,\n        }\n        if txo.is_claim:\n            if txo.can_decode_claim:\n                claim = txo.claim\n                row['txo_type'] = TXO_TYPES.get(claim.claim_type, TXO_TYPES['stream'])\n                if claim.is_repost:\n                    row['reposted_claim_id'] = claim.repost.reference.claim_id\n                    row['has_source'] = True\n                if claim.is_signed:\n                    row['channel_id'] = claim.signing_channel_id\n                if claim.is_stream:\n                    row['has_source'] = claim.stream.has_source\n            else:\n                row['txo_type'] = TXO_TYPES['stream']\n        elif txo.is_support:\n            row['txo_type'] = TXO_TYPES['support']\n            support = txo.can_decode_support\n            if support and support.is_signed:\n                row['channel_id'] = support.signing_channel_id\n        elif txo.purchase is not None:\n            row['txo_type'] = TXO_TYPES['purchase']\n            row['claim_id'] = txo.purchased_claim_id\n        if txo.script.is_claim_involved:\n            row['claim_id'] = txo.claim_id\n            row['claim_name'] = txo.claim_name\n        return row\n\n    def tx_to_row(self, tx):\n        row = {\n            'txid': tx.id,\n            'raw': sqlite3.Binary(tx.raw),\n            'height': tx.height,\n            'position': tx.position,\n            'is_verified': tx.is_verified,\n            'day': tx.get_julian_day(self.ledger),\n        }\n        txos = tx.outputs\n        if len(txos) >= 2 and txos[1].can_decode_purchase_data:\n            txos[0].purchase = txos[1]\n            row['purchased_claim_id'] = txos[1].purchase_data.claim_id\n        return row\n\n    async def insert_transaction(self, tx):\n        await self.db.execute_fetchall(*self._insert_sql('tx', self.tx_to_row(tx)))\n\n    async def update_transaction(self, tx):\n        await self.db.execute_fetchall(*self._update_sql(\"tx\", {\n            'height': tx.height, 'position': tx.position, 'is_verified': tx.is_verified\n        }, 'txid = ?', (tx.id,)))\n\n    def _transaction_io(self, conn: sqlite3.Connection, tx: Transaction, address, txhash):\n        conn.execute(*self._insert_sql('tx', self.tx_to_row(tx), replace=True)).fetchall()\n\n        is_my_input = False\n\n        for txi in tx.inputs:\n            if txi.txo_ref.txo is not None:\n                txo = txi.txo_ref.txo\n                if txo.has_address and txo.get_address(self.ledger) == address:\n                    is_my_input = True\n                    conn.execute(*self._insert_sql(\"txi\", {\n                        'txid': tx.id,\n                        'txoid': txo.id,\n                        'address': address,\n                        'position': txi.position\n                    }, ignore_duplicate=True)).fetchall()\n\n        for txo in tx.outputs:\n            if txo.script.is_pay_pubkey_hash and (txo.pubkey_hash == txhash or is_my_input):\n                conn.execute(*self._insert_sql(\n                    \"txo\", self.txo_to_row(tx, txo), ignore_duplicate=True\n                )).fetchall()\n            elif txo.script.is_pay_script_hash and is_my_input:\n                conn.execute(*self._insert_sql(\n                    \"txo\", self.txo_to_row(tx, txo), ignore_duplicate=True\n                )).fetchall()\n\n    def save_transaction_io(self, tx: Transaction, address, txhash, history):\n        return self.save_transaction_io_batch([tx], address, txhash, history)\n\n    def save_transaction_io_batch(self, txs: Iterable[Transaction], address, txhash, history):\n        history_count = history.count(':') // 2\n\n        def __many(conn):\n            for tx in txs:\n                self._transaction_io(conn, tx, address, txhash)\n            conn.execute(\n                \"UPDATE pubkey_address SET history = ?, used_times = ? WHERE address = ?\",\n                (history, history_count, address)\n            ).fetchall()\n\n        return self.db.run(__many)\n\n    async def reserve_outputs(self, txos, is_reserved=True):\n        txoids = [(is_reserved, txo.id) for txo in txos]\n        await self.db.executemany(\"UPDATE txo SET is_reserved = ? WHERE txoid = ?\", txoids)\n\n    async def release_outputs(self, txos):\n        await self.reserve_outputs(txos, is_reserved=False)\n\n    async def rewind_blockchain(self, above_height):  # pylint: disable=no-self-use\n        # TODO:\n        # 1. delete transactions above_height\n        # 2. update address histories removing deleted TXs\n        return True\n\n    async def get_spendable_utxos(self, ledger, reserve_amount, accounts: Optional[Iterable], min_amount: int = 1,\n                                  fee_per_byte: int = 50, set_reserved: bool = True,\n                                  return_insufficient_funds: bool = False) -> List:\n        to_spend = await self.db.run(\n            get_and_reserve_spendable_utxos, tuple(account.id for account in accounts), reserve_amount, min_amount,\n            fee_per_byte, set_reserved, return_insufficient_funds\n        )\n        txos = []\n        for (raw, height, verified), positions in to_spend.items():\n            tx = Transaction(raw, height=height, is_verified=verified)\n            for nout in positions:\n                txos.append(tx.outputs[nout].get_estimator(ledger))\n        return txos\n\n    async def select_transactions(self, cols, accounts=None, read_only=False, **constraints):\n        if not {'txid', 'txid__in'}.intersection(constraints):\n            assert accounts, \"'accounts' argument required when no 'txid' constraint is present\"\n            where, values = constraints_to_sql({\n                '$$account_address.account__in': [a.public_key.address for a in accounts]\n            })\n            constraints['txid__in'] = f\"\"\"\n                SELECT txo.txid FROM txo JOIN account_address USING (address) WHERE {where}\n              UNION\n                SELECT txi.txid FROM txi JOIN account_address USING (address) WHERE {where}\n            \"\"\"\n            constraints.update(values)\n        return await self.db.execute_fetchall(\n            *query(f\"SELECT {cols} FROM tx\", **constraints), read_only=read_only\n        )\n\n    TXO_NOT_MINE = Output(None, None, is_my_output=False)\n\n    async def get_transactions(self, wallet=None, **constraints):\n        include_is_spent = constraints.pop('include_is_spent', False)\n        include_is_my_input = constraints.pop('include_is_my_input', False)\n        include_is_my_output = constraints.pop('include_is_my_output', False)\n\n        tx_rows = await self.select_transactions(\n            'txid, raw, height, position, is_verified',\n            order_by=constraints.pop('order_by', [\"height=0 DESC\", \"height DESC\", \"position DESC\"]),\n            **constraints\n        )\n\n        if not tx_rows:\n            return []\n\n        txids, txs, txi_txoids = [], [], []\n        for row in tx_rows:\n            txids.append(row['txid'])\n            txs.append(Transaction(\n                raw=row['raw'], height=row['height'], position=row['position'],\n                is_verified=bool(row['is_verified'])\n            ))\n            for txi in txs[-1].inputs:\n                txi_txoids.append(txi.txo_ref.id)\n\n        step = self.MAX_QUERY_VARIABLES\n        annotated_txos = {}\n        for offset in range(0, len(txids), step):\n            annotated_txos.update({\n                txo.id: txo for txo in\n                (await self.get_txos(\n                    wallet=wallet,\n                    txid__in=txids[offset:offset+step], order_by='txo.txid',\n                    include_is_spent=include_is_spent,\n                    include_is_my_input=include_is_my_input,\n                    include_is_my_output=include_is_my_output,\n                ))\n            })\n\n        referenced_txos = {}\n        for offset in range(0, len(txi_txoids), step):\n            referenced_txos.update({\n                txo.id: txo for txo in\n                (await self.get_txos(\n                    wallet=wallet,\n                    txoid__in=txi_txoids[offset:offset+step], order_by='txo.txoid',\n                    include_is_my_output=include_is_my_output,\n                ))\n            })\n\n        for tx in txs:\n            for txi in tx.inputs:\n                txo = referenced_txos.get(txi.txo_ref.id)\n                if txo:\n                    txi.txo_ref = txo.ref\n            for txo in tx.outputs:\n                _txo = annotated_txos.get(txo.id)\n                if _txo:\n                    txo.update_annotations(_txo)\n                else:\n                    txo.update_annotations(self.TXO_NOT_MINE)\n\n        for tx in txs:\n            txos = tx.outputs\n            if len(txos) >= 2 and txos[1].can_decode_purchase_data:\n                txos[0].purchase = txos[1]\n\n        return txs\n\n    async def get_transaction_count(self, **constraints):\n        constraints.pop('wallet', None)\n        constraints.pop('offset', None)\n        constraints.pop('limit', None)\n        constraints.pop('order_by', None)\n        count = await self.select_transactions('COUNT(*) as total', **constraints)\n        return count[0]['total'] or 0\n\n    async def get_transaction(self, **constraints):\n        txs = await self.get_transactions(limit=1, **constraints)\n        if txs:\n            return txs[0]\n\n    async def select_txos(\n            self, cols, accounts=None, is_my_input=None, is_my_output=True,\n            is_my_input_or_output=None, exclude_internal_transfers=False,\n            include_is_spent=False, include_is_my_input=False,\n            is_spent=None, read_only=False, **constraints):\n        for rename_col in ('txid', 'txoid'):\n            for rename_constraint in (rename_col, rename_col+'__in', rename_col+'__not_in'):\n                if rename_constraint in constraints:\n                    constraints['txo.'+rename_constraint] = constraints.pop(rename_constraint)\n        if accounts:\n            account_in_sql, values = constraints_to_sql({\n                '$$account__in': [a.public_key.address for a in accounts]\n            })\n            my_addresses = f\"SELECT address FROM account_address WHERE {account_in_sql}\"\n            constraints.update(values)\n            if is_my_input_or_output:\n                include_is_my_input = True\n                constraints['received_or_sent__or'] = {\n                    'txo.address__in': my_addresses,\n                    'sent__and': {\n                        'txi.address__is_not_null': True,\n                        'txi.address__in': my_addresses\n                    }\n                }\n            else:\n                if is_my_output:\n                    constraints['txo.address__in'] = my_addresses\n                elif is_my_output is False:\n                    constraints['txo.address__not_in'] = my_addresses\n                if is_my_input:\n                    include_is_my_input = True\n                    constraints['txi.address__is_not_null'] = True\n                    constraints['txi.address__in'] = my_addresses\n                elif is_my_input is False:\n                    include_is_my_input = True\n                    constraints['is_my_input_false__or'] = {\n                        'txi.address__is_null': True,\n                        'txi.address__not_in': my_addresses\n                    }\n            if exclude_internal_transfers:\n                include_is_my_input = True\n                constraints['exclude_internal_payments__or'] = {\n                    'txo.txo_type__not': TXO_TYPES['other'],\n                    'txo.address__not_in': my_addresses,\n                    'txi.address__is_null': True,\n                    'txi.address__not_in': my_addresses,\n                }\n        sql = [f\"SELECT {cols} FROM txo JOIN tx ON (tx.txid=txo.txid)\"]\n        if is_spent:\n            constraints['spent.txoid__is_not_null'] = True\n        elif is_spent is False:\n            constraints['is_reserved'] = False\n            constraints['spent.txoid__is_null'] = True\n        if include_is_spent or is_spent is not None:\n            sql.append(\"LEFT JOIN txi AS spent ON (spent.txoid=txo.txoid)\")\n        if include_is_my_input:\n            sql.append(\"LEFT JOIN txi ON (txi.position=0 AND txi.txid=txo.txid)\")\n        return await self.db.execute_fetchall(*query(' '.join(sql), **constraints), read_only=read_only)\n\n    async def get_txos(\n        self, wallet=None, no_tx=False, no_channel_info=False, read_only=False, **constraints\n    ) -> List[Output]:\n        include_is_spent = constraints.get('include_is_spent', False)\n        include_is_my_input = constraints.get('include_is_my_input', False)\n        include_is_my_output = constraints.pop('include_is_my_output', False)\n        include_received_tips = constraints.pop('include_received_tips', False)\n\n        select_columns = [\n            \"tx.txid, tx.height, tx.position as tx_position, tx.is_verified, \"\n            \"txo_type, txo.position as txo_position, amount, script\"\n        ]\n        if not no_tx:\n            select_columns.append(\"raw\")\n\n        my_accounts = {a.public_key.address for a in wallet.accounts} if wallet else set()\n        my_accounts_sql = \"\"\n        if include_is_my_output or include_is_my_input:\n            my_accounts_sql, values = constraints_to_sql({'$$account__in#_wallet': my_accounts})\n            constraints.update(values)\n\n        if include_is_my_output and my_accounts:\n            if constraints.get('is_my_output', None) in (True, False):\n                select_columns.append(f\"{1 if constraints['is_my_output'] else 0} AS is_my_output\")\n            else:\n                select_columns.append(f\"\"\"(\n                    txo.address IN (SELECT address FROM account_address WHERE {my_accounts_sql})\n                ) AS is_my_output\"\"\")\n\n        if include_is_my_input and my_accounts:\n            if constraints.get('is_my_input', None) in (True, False):\n                select_columns.append(f\"{1 if constraints['is_my_input'] else 0} AS is_my_input\")\n            else:\n                select_columns.append(f\"\"\"(\n                    txi.address IS NOT NULL AND\n                    txi.address IN (SELECT address FROM account_address WHERE {my_accounts_sql})\n                ) AS is_my_input\"\"\")\n\n        if include_is_spent:\n            select_columns.append(\"spent.txoid IS NOT NULL AS is_spent\")\n\n        if include_received_tips:\n            select_columns.append(f\"\"\"(\n            SELECT COALESCE(SUM(support.amount), 0) FROM txo AS support WHERE\n                support.claim_id = txo.claim_id AND\n                support.txo_type = {TXO_TYPES['support']} AND\n                support.address IN (SELECT address FROM account_address WHERE {my_accounts_sql}) AND\n                support.txoid NOT IN (SELECT txoid FROM txi)\n            ) AS received_tips\"\"\")\n\n        if 'order_by' not in constraints or constraints['order_by'] == 'height':\n            constraints['order_by'] = [\n                \"tx.height in (0, -1) DESC\", \"tx.height DESC\", \"tx.position DESC\", \"txo.position\"\n            ]\n        elif constraints.get('order_by', None) == 'none':\n            del constraints['order_by']\n\n        rows = await self.select_txos(', '.join(select_columns), read_only=read_only, **constraints)\n\n        txos = []\n        txs = {}\n        for row in rows:\n            if no_tx:\n                txo = Output(\n                    amount=row['amount'],\n                    script=OutputScript(row['script']),\n                    tx_ref=TXRefImmutable.from_id(row['txid'], row['height']),\n                    position=row['txo_position']\n                )\n            else:\n                if row['txid'] not in txs:\n                    txs[row['txid']] = Transaction(\n                        row['raw'], height=row['height'], position=row['tx_position'],\n                        is_verified=bool(row['is_verified'])\n                    )\n                txo = txs[row['txid']].outputs[row['txo_position']]\n            if include_is_spent:\n                txo.is_spent = bool(row['is_spent'])\n            if include_is_my_input:\n                txo.is_my_input = bool(row['is_my_input'])\n            if include_is_my_output:\n                txo.is_my_output = bool(row['is_my_output'])\n            if include_is_my_input and include_is_my_output:\n                if txo.is_my_input and txo.is_my_output and row['txo_type'] == TXO_TYPES['other']:\n                    txo.is_internal_transfer = True\n                else:\n                    txo.is_internal_transfer = False\n            if include_received_tips:\n                txo.received_tips = row['received_tips']\n            txos.append(txo)\n\n        if not no_channel_info:\n            channel_ids = set()\n            for txo in txos:\n                if txo.is_claim and txo.can_decode_claim:\n                    if txo.claim.is_signed:\n                        channel_ids.add(txo.claim.signing_channel_id)\n                    if txo.claim.is_channel and wallet:\n                        for account in wallet.accounts:\n                            private_key = await account.get_channel_private_key(\n                                txo.claim.channel.public_key_bytes\n                            )\n                            if private_key:\n                                txo.private_key = private_key\n                                break\n\n            if channel_ids:\n                channels = {\n                    txo.claim_id: txo for txo in\n                    (await self.get_channels(\n                        wallet=wallet,\n                        claim_id__in=channel_ids,\n                        read_only=read_only\n                    ))\n                }\n                for txo in txos:\n                    if txo.is_claim and txo.can_decode_claim:\n                        txo.channel = channels.get(txo.claim.signing_channel_id, None)\n\n        return txos\n\n    @staticmethod\n    def _clean_txo_constraints_for_aggregation(constraints):\n        constraints.pop('include_is_spent', None)\n        constraints.pop('include_is_my_input', None)\n        constraints.pop('include_is_my_output', None)\n        constraints.pop('include_received_tips', None)\n        constraints.pop('wallet', None)\n        constraints.pop('resolve', None)\n        constraints.pop('offset', None)\n        constraints.pop('limit', None)\n        constraints.pop('order_by', None)\n\n    async def get_txo_count(self, **constraints):\n        self._clean_txo_constraints_for_aggregation(constraints)\n        count = await self.select_txos('COUNT(*) AS total', **constraints)\n        return count[0]['total'] or 0\n\n    async def get_txo_sum(self, **constraints):\n        self._clean_txo_constraints_for_aggregation(constraints)\n        result = await self.select_txos('SUM(amount) AS total', **constraints)\n        return result[0]['total'] or 0\n\n    async def get_txo_plot(self, start_day=None, days_back=0, end_day=None, days_after=None, **constraints):\n        self._clean_txo_constraints_for_aggregation(constraints)\n        if start_day is None:\n            constraints['day__gte'] = self.ledger.headers.estimated_julian_day(\n                self.ledger.headers.height\n            ) - days_back\n        else:\n            constraints['day__gte'] = date_to_julian_day(\n                date.fromisoformat(start_day)\n            )\n            if end_day is not None:\n                constraints['day__lte'] = date_to_julian_day(\n                    date.fromisoformat(end_day)\n                )\n            elif days_after is not None:\n                constraints['day__lte'] = constraints['day__gte'] + days_after\n        return await self.select_txos(\n            \"DATE(day) AS day, SUM(amount) AS total\",\n            group_by='day', order_by='day', **constraints\n        )\n\n    def get_utxos(self, read_only=False, **constraints):\n        return self.get_txos(is_spent=False, read_only=read_only, **constraints)\n\n    def get_utxo_count(self, **constraints):\n        return self.get_txo_count(is_spent=False, **constraints)\n\n    async def get_balance(self, wallet=None, accounts=None, read_only=False, **constraints):\n        assert wallet or accounts, \\\n            \"'wallet' or 'accounts' constraints required to calculate balance\"\n        constraints['accounts'] = accounts or wallet.accounts\n        balance = await self.select_txos(\n            'SUM(amount) as total', is_spent=False, read_only=read_only, **constraints\n        )\n        return balance[0]['total'] or 0\n\n    async def get_detailed_balance(self, accounts, read_only=False, **constraints):\n        constraints['accounts'] = accounts\n        result = (await self.select_txos(\n            f\"COALESCE(SUM(amount), 0) AS total,\"\n            f\"COALESCE(SUM(\"\n            f\"  CASE WHEN\"\n            f\"    txo_type NOT IN ({TXO_TYPES['other']}, {TXO_TYPES['purchase']})\"\n            f\"  THEN amount ELSE 0 END), 0) AS reserved,\"\n            f\"COALESCE(SUM(\"\n            f\"  CASE WHEN\"\n            f\"    txo_type IN ({','.join(map(str, CLAIM_TYPES))})\"\n            f\"  THEN amount ELSE 0 END), 0) AS claims,\"\n            f\"COALESCE(SUM(CASE WHEN txo_type = {TXO_TYPES['support']} THEN amount ELSE 0 END), 0) AS supports,\"\n            f\"COALESCE(SUM(\"\n            f\"  CASE WHEN\"\n            f\"    txo_type = {TXO_TYPES['support']} AND\"\n            f\"    TXI.address IS NOT NULL AND\"\n            f\"    TXI.address IN (SELECT address FROM account_address WHERE account = :$account__in0)\"\n            f\"  THEN amount ELSE 0 END), 0) AS my_supports\",\n            is_spent=False,\n            include_is_my_input=True,\n            read_only=read_only,\n            **constraints\n        ))[0]\n        return {\n            \"total\": result[\"total\"],\n            \"available\": result[\"total\"] - result[\"reserved\"],\n            \"reserved\": result[\"reserved\"],\n            \"reserved_subtotals\": {\n                \"claims\": result[\"claims\"],\n                \"supports\": result[\"my_supports\"],\n                \"tips\": result[\"supports\"] - result[\"my_supports\"]\n            }\n        }\n\n    async def select_addresses(self, cols, read_only=False, **constraints):\n        return await self.db.execute_fetchall(*query(\n            f\"SELECT {cols} FROM pubkey_address JOIN account_address USING (address)\",\n            **constraints\n        ), read_only=read_only)\n\n    async def get_addresses(self, cols=None, read_only=False, **constraints):\n        cols = cols or (\n            'address', 'account', 'chain', 'history', 'used_times',\n            'pubkey', 'chain_code', 'n', 'depth'\n        )\n        addresses = await self.select_addresses(', '.join(cols), read_only=read_only, **constraints)\n        if 'pubkey' in cols:\n            for address in addresses:\n                address['pubkey'] = PublicKey(\n                    self.ledger, address.pop('pubkey'), address.pop('chain_code'),\n                    address.pop('n'), address.pop('depth')\n                )\n        return addresses\n\n    async def get_address_count(self, cols=None, read_only=False, **constraints):\n        self._clean_txo_constraints_for_aggregation(constraints)\n        count = await self.select_addresses('COUNT(*) as total', read_only=read_only, **constraints)\n        return count[0]['total'] or 0\n\n    async def get_address(self, read_only=False, **constraints):\n        addresses = await self.get_addresses(read_only=read_only, limit=1, **constraints)\n        if addresses:\n            return addresses[0]\n\n    async def add_keys(self, account, chain, pubkeys):\n        await self.db.executemany(\n            \"insert or ignore into account_address \"\n            \"(account, address, chain, pubkey, chain_code, n, depth) values \"\n            \"(?, ?, ?, ?, ?, ?, ?)\", ((\n                account.id, k.address, chain,\n                sqlite3.Binary(k.pubkey_bytes),\n                sqlite3.Binary(k.chain_code),\n                k.n, k.depth\n            ) for k in pubkeys)\n        )\n        await self.db.executemany(\n            \"insert or ignore into pubkey_address (address) values (?)\",\n            ((pubkey.address,) for pubkey in pubkeys)\n        )\n\n    async def _set_address_history(self, address, history):\n        await self.db.execute_fetchall(\n            \"UPDATE pubkey_address SET history = ?, used_times = ? WHERE address = ?\",\n            (history, history.count(':')//2, address)\n        )\n\n    async def set_address_history(self, address, history):\n        await self._set_address_history(address, history)\n\n    async def is_channel_key_used(self, account, key: PublicKey):\n        channels = await self.get_txos(\n            accounts=[account], txo_type=TXO_TYPES['channel'],\n            no_tx=True, no_channel_info=True\n        )\n        other_key_bytes = key.pubkey_bytes\n        for channel in channels:\n            claim = channel.can_decode_claim\n            if claim and claim.channel.public_key_bytes == other_key_bytes:\n                return True\n        return False\n\n    @staticmethod\n    def constrain_purchases(constraints):\n        accounts = constraints.pop('accounts', None)\n        assert accounts, \"'accounts' argument required to find purchases\"\n        if not {'purchased_claim_id', 'purchased_claim_id__in'}.intersection(constraints):\n            constraints['purchased_claim_id__is_not_null'] = True\n        constraints.update({\n            f'$account{i}': a.public_key.address for i, a in enumerate(accounts)\n        })\n        account_values = ', '.join([f':$account{i}' for i in range(len(accounts))])\n        constraints['txid__in'] = f\"\"\"\n            SELECT txid FROM txi JOIN account_address USING (address)\n            WHERE account_address.account IN ({account_values})\n        \"\"\"\n\n    async def get_purchases(self, **constraints):\n        self.constrain_purchases(constraints)\n        return [tx.outputs[0] for tx in await self.get_transactions(**constraints)]\n\n    def get_purchase_count(self, **constraints):\n        self.constrain_purchases(constraints)\n        return self.get_transaction_count(**constraints)\n\n    @staticmethod\n    def constrain_claims(constraints):\n        if {'txo_type', 'txo_type__in'}.intersection(constraints):\n            return\n        claim_types = constraints.pop('claim_type', None)\n        if claim_types:\n            constrain_single_or_list(\n                constraints, 'txo_type', claim_types, lambda x: TXO_TYPES[x]\n            )\n        else:\n            constraints['txo_type__in'] = CLAIM_TYPES\n\n    async def get_claims(self, read_only=False, **constraints) -> List[Output]:\n        self.constrain_claims(constraints)\n        return await self.get_utxos(read_only=read_only, **constraints)\n\n    def get_claim_count(self, **constraints):\n        self.constrain_claims(constraints)\n        return self.get_utxo_count(**constraints)\n\n    @staticmethod\n    def constrain_streams(constraints):\n        constraints['txo_type'] = TXO_TYPES['stream']\n\n    def get_streams(self, read_only=False, **constraints):\n        self.constrain_streams(constraints)\n        return self.get_claims(read_only=read_only, **constraints)\n\n    def get_stream_count(self, **constraints):\n        self.constrain_streams(constraints)\n        return self.get_claim_count(**constraints)\n\n    @staticmethod\n    def constrain_channels(constraints):\n        constraints['txo_type'] = TXO_TYPES['channel']\n\n    def get_channels(self, **constraints):\n        self.constrain_channels(constraints)\n        return self.get_claims(**constraints)\n\n    def get_channel_count(self, **constraints):\n        self.constrain_channels(constraints)\n        return self.get_claim_count(**constraints)\n\n    @staticmethod\n    def constrain_supports(constraints):\n        constraints['txo_type'] = TXO_TYPES['support']\n\n    def get_supports(self, **constraints):\n        self.constrain_supports(constraints)\n        return self.get_utxos(**constraints)\n\n    def get_support_count(self, **constraints):\n        self.constrain_supports(constraints)\n        return self.get_utxo_count(**constraints)\n\n    @staticmethod\n    def constrain_collections(constraints):\n        constraints['txo_type'] = TXO_TYPES['collection']\n\n    def get_collections(self, **constraints):\n        self.constrain_collections(constraints)\n        return self.get_utxos(**constraints)\n\n    def get_collection_count(self, **constraints):\n        self.constrain_collections(constraints)\n        return self.get_utxo_count(**constraints)\n\n    async def release_all_outputs(self, account=None):\n        if account is None:\n            await self.db.execute_fetchall(\"UPDATE txo SET is_reserved = 0 WHERE is_reserved = 1\")\n        else:\n            await self.db.execute_fetchall(\n                \"UPDATE txo SET is_reserved = 0 WHERE\"\n                \"  is_reserved = 1 AND txo.address IN (\"\n                \"    SELECT address from account_address WHERE account = ?\"\n                \"  )\", (account.public_key.address, )\n            )\n\n    def get_supports_summary(self, read_only=False, **constraints):\n        return self.get_txos(\n            txo_type=TXO_TYPES['support'],\n            is_spent=False, is_my_output=True,\n            include_is_my_input=True,\n            no_tx=True, read_only=read_only,\n            **constraints\n        )\n"
  },
  {
    "path": "lbry/wallet/dewies.py",
    "content": "import textwrap\nfrom .util import coins_to_satoshis, satoshis_to_coins\n\n\ndef lbc_to_dewies(lbc: str) -> int:\n    try:\n        return coins_to_satoshis(lbc)\n    except ValueError:\n        raise ValueError(textwrap.dedent(\n            f\"\"\"\n            Decimal inputs require a value in the ones place and in the tenths place\n            separated by a period. The value provided, '{lbc}', is not of the correct\n            format.\n\n            The following are examples of valid decimal inputs:\n\n            1.0\n            0.001\n            2.34500\n            4534.4\n            2323434.0000\n\n            The following are NOT valid:\n\n            83\n            .456\n            123.\n            \"\"\"\n        ))\n\n\ndef dewies_to_lbc(dewies) -> str:\n    return satoshis_to_coins(dewies)\n\n\ndef dict_values_to_lbc(d):\n    lbc_dict = {}\n    for key, value in d.items():\n        if isinstance(value, int):\n            lbc_dict[key] = dewies_to_lbc(value)\n        elif isinstance(value, dict):\n            lbc_dict[key] = dict_values_to_lbc(value)\n        else:\n            lbc_dict[key] = value\n    return lbc_dict\n"
  },
  {
    "path": "lbry/wallet/hash.py",
    "content": "from binascii import hexlify, unhexlify\nfrom .constants import NULL_HASH32\n\n\nclass TXRef:\n\n    __slots__ = '_id', '_hash'\n\n    def __init__(self):\n        self._id = None\n        self._hash = None\n\n    @property\n    def id(self):\n        return self._id\n\n    @property\n    def hash(self):\n        return self._hash\n\n    @property\n    def height(self):\n        return -1\n\n    @property\n    def is_null(self):\n        return self.hash == NULL_HASH32\n\n\nclass TXRefImmutable(TXRef):\n\n    __slots__ = ('_height',)\n\n    def __init__(self):\n        super().__init__()\n        self._height = -1\n\n    @classmethod\n    def from_hash(cls, tx_hash: bytes, height: int) -> 'TXRefImmutable':\n        ref = cls()\n        ref._hash = tx_hash\n        ref._id = hexlify(tx_hash[::-1]).decode()\n        ref._height = height\n        return ref\n\n    @classmethod\n    def from_id(cls, tx_id: str, height: int) -> 'TXRefImmutable':\n        ref = cls()\n        ref._id = tx_id\n        ref._hash = unhexlify(tx_id)[::-1]\n        ref._height = height\n        return ref\n\n    @property\n    def height(self):\n        return self._height\n"
  },
  {
    "path": "lbry/wallet/header.py",
    "content": "import base64\nimport os\nimport struct\nimport asyncio\nimport logging\nimport zlib\nfrom datetime import date\n\nfrom io import BytesIO\nfrom typing import Optional, Iterator, Tuple, Callable\nfrom binascii import hexlify, unhexlify\n\nfrom lbry.crypto.hash import sha512, double_sha256, ripemd160\nfrom lbry.wallet.util import ArithUint256, date_to_julian_day\nfrom .checkpoints import HASHES\n\n\nlog = logging.getLogger(__name__)\n\n\nclass InvalidHeader(Exception):\n\n    def __init__(self, height, message):\n        super().__init__(message)\n        self.message = message\n        self.height = height\n\n\nclass Headers:\n\n    header_size = 112\n    chunk_size = 10**16\n\n    max_target = 0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n    genesis_hash = b'9c89283ba0f3227f6c03b70216b9f665f0118d5e0fa729cedf4fb34d6a34f463'\n    target_timespan = 150\n    checkpoints = HASHES\n    first_block_timestamp = 1466646588  # block 1, as 0 is off by a lot\n    timestamp_average_offset = 160.6855883050695  # calculated at 733447\n\n    validate_difficulty: bool = True\n\n    def __init__(self, path) -> None:\n        self.io = None\n        self.path = path\n        self._size: Optional[int] = None\n        self.chunk_getter: Optional[Callable] = None\n        self.known_missing_checkpointed_chunks = set()\n        self.check_chunk_lock = asyncio.Lock()\n\n    async def open(self):\n        self.io = BytesIO()\n        if self.path != ':memory:':\n            def _readit():\n                if os.path.exists(self.path):\n                    with open(self.path, 'r+b') as header_file:\n                        self.io.seek(0)\n                        self.io.write(header_file.read())\n            await asyncio.get_event_loop().run_in_executor(None, _readit)\n        bytes_size = self.io.seek(0, os.SEEK_END)\n        self._size = bytes_size // self.header_size\n        max_checkpointed_height = max(self.checkpoints.keys() or [-1]) + 1000\n        if bytes_size % self.header_size:\n            log.warning(\"Reader file size doesnt match header size. Repairing, might take a while.\")\n            await self.repair()\n        else:\n            # try repairing any incomplete write on tip from previous runs (outside of checkpoints, that are ok)\n            await self.repair(start_height=max_checkpointed_height)\n        await self.ensure_checkpointed_size()\n        await self.get_all_missing_headers()\n\n    async def close(self):\n        if self.io is not None:\n            def _close():\n                flags = 'r+b' if os.path.exists(self.path) else 'w+b'\n                with open(self.path, flags) as header_file:\n                    header_file.write(self.io.getbuffer())\n            await asyncio.get_event_loop().run_in_executor(None, _close)\n            self.io.close()\n            self.io = None\n\n    @staticmethod\n    def serialize(header):\n        return b''.join([\n            struct.pack('<I', header['version']),\n            unhexlify(header['prev_block_hash'])[::-1],\n            unhexlify(header['merkle_root'])[::-1],\n            unhexlify(header['claim_trie_root'])[::-1],\n            struct.pack('<III', header['timestamp'], header['bits'], header['nonce'])\n        ])\n\n    @staticmethod\n    def deserialize(height, header):\n        version, = struct.unpack('<I', header[:4])\n        timestamp, bits, nonce = struct.unpack('<III', header[100:112])\n        return {\n            'version': version,\n            'prev_block_hash': hexlify(header[4:36][::-1]),\n            'merkle_root': hexlify(header[36:68][::-1]),\n            'claim_trie_root': hexlify(header[68:100][::-1]),\n            'timestamp': timestamp,\n            'bits': bits,\n            'nonce': nonce,\n            'block_height': height,\n        }\n\n    def get_next_chunk_target(self, chunk: int) -> ArithUint256:\n        return ArithUint256(self.max_target)\n\n    def get_next_block_target(self, max_target: ArithUint256, previous: Optional[dict],\n                              current: Optional[dict]) -> ArithUint256:\n        # https://github.com/lbryio/lbrycrd/blob/master/src/lbry.cpp\n        if previous is None and current is None:\n            return max_target\n        if previous is None:\n            previous = current\n        actual_timespan = current['timestamp'] - previous['timestamp']\n        modulated_timespan = self.target_timespan + int((actual_timespan - self.target_timespan) / 8)\n        minimum_timespan = self.target_timespan - int(self.target_timespan / 8)  # 150 - 18 = 132\n        maximum_timespan = self.target_timespan + int(self.target_timespan / 2)  # 150 + 75 = 225\n        clamped_timespan = max(minimum_timespan, min(modulated_timespan, maximum_timespan))\n        target = ArithUint256.from_compact(current['bits'])\n        new_target = min(max_target, (target * clamped_timespan) / self.target_timespan)\n        return new_target\n\n    def __len__(self) -> int:\n        return self._size\n\n    def __bool__(self):\n        return True\n\n    async def get(self, height) -> dict:\n        if isinstance(height, slice):\n            raise NotImplementedError(\"Slicing of header chain has not been implemented yet.\")\n        try:\n            return self.deserialize(height, await self.get_raw_header(height))\n        except struct.error:\n            raise IndexError(f\"failed to get {height}, at {len(self)}\")\n\n    def estimated_timestamp(self, height, try_real_headers=True):\n        if height <= 0:\n            return\n        if try_real_headers and self.has_header(height):\n            offset = height * self.header_size\n            return struct.unpack('<I', self.io.getbuffer()[offset + 100: offset + 104])[0]\n        return int(self.first_block_timestamp + (height * self.timestamp_average_offset))\n\n    def estimated_julian_day(self, height):\n        return date_to_julian_day(date.fromtimestamp(self.estimated_timestamp(height, False)))\n\n    async def get_raw_header(self, height) -> bytes:\n        if self.chunk_getter:\n            await self.ensure_chunk_at(height)\n        if not 0 <= height <= self.height:\n            raise IndexError(f\"{height} is out of bounds, current height: {self.height}\")\n        return self._read(height)\n\n    def _read(self, height, count=1):\n        offset = height * self.header_size\n        return bytes(self.io.getbuffer()[offset: offset + self.header_size * count])\n\n    def chunk_hash(self, start, count):\n        return self.hash_header(self._read(start, count)).decode()\n\n    async def ensure_checkpointed_size(self):\n        max_checkpointed_height = max(self.checkpoints.keys() or [-1])\n        if self.height < max_checkpointed_height:\n            self._write(max_checkpointed_height, bytes([0] * self.header_size * 1000))\n\n    async def ensure_chunk_at(self, height):\n        async with self.check_chunk_lock:\n            if self.has_header(height):\n                log.debug(\"has header %s\", height)\n                return\n            return await self.fetch_chunk(height)\n\n    async def fetch_chunk(self, height):\n        log.info(\"on-demand fetching height %s\", height)\n        start = (height // 1000) * 1000\n        headers = await self.chunk_getter(start)  # pylint: disable=not-callable\n        chunk = (\n            zlib.decompress(base64.b64decode(headers['base64']), wbits=-15, bufsize=600_000)\n        )\n        chunk_hash = self.hash_header(chunk).decode()\n        if self.checkpoints.get(start) == chunk_hash:\n            self._write(start, chunk)\n            if start in self.known_missing_checkpointed_chunks:\n                self.known_missing_checkpointed_chunks.remove(start)\n            return\n        elif start not in self.checkpoints:\n            return  # todo: fixme\n        raise Exception(\n            f\"Checkpoint mismatch at height {start}. Expected {self.checkpoints[start]}, but got {chunk_hash} instead.\"\n        )\n\n    def has_header(self, height):\n        normalized_height = (height // 1000) * 1000\n        if normalized_height in self.checkpoints:\n            return normalized_height not in self.known_missing_checkpointed_chunks\n\n        empty = '56944c5d3f98413ef45cf54545538103cc9f298e0575820ad3591376e2e0f65d'\n        all_zeroes = '789d737d4f448e554b318c94063bbfa63e9ccda6e208f5648ca76ee68896557b'\n        return self.chunk_hash(height, 1) not in (empty, all_zeroes)\n\n    async def get_all_missing_headers(self):\n        # Heavy operation done in one optimized shot\n        for chunk_height, expected_hash in reversed(list(self.checkpoints.items())):\n            if chunk_height in self.known_missing_checkpointed_chunks:\n                continue\n            if self.chunk_hash(chunk_height, 1000) != expected_hash:\n                self.known_missing_checkpointed_chunks.add(chunk_height)\n        return self.known_missing_checkpointed_chunks\n\n    @property\n    def height(self) -> int:\n        return len(self)-1\n\n    @property\n    def bytes_size(self):\n        return len(self) * self.header_size\n\n    async def hash(self, height=None) -> bytes:\n        return self.hash_header(\n            await self.get_raw_header(height if height is not None else self.height)\n        )\n\n    @staticmethod\n    def hash_header(header: bytes) -> bytes:\n        if header is None:\n            return b'0' * 64\n        return hexlify(double_sha256(header)[::-1])\n\n    async def connect(self, start: int, headers: bytes) -> int:\n        added = 0\n        bail = False\n        for height, chunk in self._iterate_chunks(start, headers):\n            try:\n                # validate_chunk() is CPU bound and reads previous chunks from file system\n                await self.validate_chunk(height, chunk)\n            except InvalidHeader as e:\n                bail = True\n                chunk = chunk[:(height-e.height)*self.header_size]\n            if chunk:\n                added += self._write(height, chunk)\n            if bail:\n                break\n        return added\n\n    def _write(self, height, verified_chunk):\n        self.io.seek(height * self.header_size, os.SEEK_SET)\n        written = self.io.write(verified_chunk) // self.header_size\n        # self.io.truncate()\n        # .seek()/.write()/.truncate() might also .flush() when needed\n        # the goal here is mainly to ensure we're definitely flush()'ing\n        self.io.flush()\n        self._size = max(self._size or 0, self.io.tell() // self.header_size)\n        return written\n\n    async def validate_chunk(self, height, chunk):\n        previous_hash, previous_header, previous_previous_header = None, None, None\n        if height > 0:\n            raw = await self.get_raw_header(height-1)\n            previous_header = self.deserialize(height-1, raw)\n            previous_hash = self.hash_header(raw)\n        if height > 1:\n            previous_previous_header = await self.get(height-2)\n        chunk_target = self.get_next_chunk_target(height // 2016 - 1)\n        for current_hash, current_header in self._iterate_headers(height, chunk):\n            block_target = self.get_next_block_target(chunk_target, previous_previous_header, previous_header)\n            self.validate_header(height, current_hash, current_header, previous_hash, block_target)\n            previous_previous_header = previous_header\n            previous_header = current_header\n            previous_hash = current_hash\n\n    def validate_header(self, height: int, current_hash: bytes,\n                        header: dict, previous_hash: bytes, target: ArithUint256):\n\n        if previous_hash is None:\n            if self.genesis_hash is not None and self.genesis_hash != current_hash:\n                raise InvalidHeader(\n                    height, f\"genesis header doesn't match: {current_hash.decode()} \"\n                            f\"vs expected {self.genesis_hash.decode()}\")\n            return\n\n        if header['prev_block_hash'] != previous_hash:\n            raise InvalidHeader(\n                height, \"previous hash mismatch: {} vs expected {}\".format(\n                    header['prev_block_hash'].decode(), previous_hash.decode())\n            )\n\n        if self.validate_difficulty:\n\n            if header['bits'] != target.compact:\n                raise InvalidHeader(\n                    height, \"bits mismatch: {} vs expected {}\".format(\n                        header['bits'], target.compact)\n                )\n\n            proof_of_work = self.get_proof_of_work(current_hash)\n            if proof_of_work > target:\n                raise InvalidHeader(\n                    height, f\"insufficient proof of work: {proof_of_work.value} vs target {target.value}\"\n                )\n\n    async def repair(self, start_height=0):\n        previous_header_hash = fail = None\n        batch_size = 36\n        for height in range(start_height, self.height, batch_size):\n            headers = self._read(height, batch_size)\n            if len(headers) % self.header_size != 0:\n                headers = headers[:(len(headers) // self.header_size) * self.header_size]\n            for header_hash, header in self._iterate_headers(height, headers):\n                height = header['block_height']\n                if previous_header_hash:\n                    if header['prev_block_hash'] != previous_header_hash:\n                        fail = True\n                elif height == 0:\n                    if header_hash != self.genesis_hash:\n                        fail = True\n                else:\n                    # for sanity and clarity, since it is the only way we can end up here\n                    assert start_height > 0 and height == start_height\n                if fail:\n                    log.warning(\"Header file corrupted at height %s, truncating it.\", height - 1)\n                    self.io.seek(max(0, (height - 1)) * self.header_size, os.SEEK_SET)\n                    self.io.truncate()\n                    self.io.flush()\n                    self._size = self.io.seek(0, os.SEEK_END) // self.header_size\n                    return\n                previous_header_hash = header_hash\n\n    @classmethod\n    def get_proof_of_work(cls, header_hash: bytes):\n        return ArithUint256(int(b'0x' + cls.header_hash_to_pow_hash(header_hash), 16))\n\n    def _iterate_chunks(self, height: int, headers: bytes) -> Iterator[Tuple[int, bytes]]:\n        assert len(headers) % self.header_size == 0, f\"{len(headers)} {len(headers)%self.header_size}\"\n        start = 0\n        end = (self.chunk_size - height % self.chunk_size) * self.header_size\n        while start < end:\n            yield height + (start // self.header_size), headers[start:end]\n            start = end\n            end = min(len(headers), end + self.chunk_size * self.header_size)\n\n    def _iterate_headers(self, height: int, headers: bytes) -> Iterator[Tuple[bytes, dict]]:\n        assert len(headers) % self.header_size == 0, len(headers)\n        for idx in range(len(headers) // self.header_size):\n            start, end = idx * self.header_size, (idx + 1) * self.header_size\n            header = headers[start:end]\n            yield self.hash_header(header), self.deserialize(height+idx, header)\n\n    @staticmethod\n    def header_hash_to_pow_hash(header_hash: bytes):\n        header_hash_bytes = unhexlify(header_hash)[::-1]\n        h = sha512(header_hash_bytes)\n        pow_hash = double_sha256(\n            ripemd160(h[:len(h) // 2]) +\n            ripemd160(h[len(h) // 2:])\n        )\n        return hexlify(pow_hash[::-1])\n\n\nclass UnvalidatedHeaders(Headers):\n    validate_difficulty = False\n    max_target = 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n    genesis_hash = b'6e3fcf1299d4ec5d79c3a4c91d624a4acf9e2e173d95a1a0504f677669687556'\n    checkpoints = {}\n"
  },
  {
    "path": "lbry/wallet/ledger.py",
    "content": "import os\nimport copy\nimport time\nimport asyncio\nimport logging\nfrom datetime import datetime\nfrom functools import partial\nfrom operator import itemgetter\nfrom collections import defaultdict\nfrom binascii import hexlify, unhexlify\nfrom typing import Dict, Tuple, Type, Iterable, List, Optional, DefaultDict, NamedTuple\n\nfrom lbry.schema.result import Outputs, INVALID, NOT_FOUND\nfrom lbry.schema.url import URL\nfrom lbry.crypto.hash import hash160, double_sha256, sha256\nfrom lbry.crypto.base58 import Base58\nfrom lbry.utils import LRUCacheWithMetrics\n\nfrom lbry.wallet.tasks import TaskGroup\nfrom lbry.wallet.database import Database\nfrom lbry.wallet.stream import StreamController\nfrom lbry.wallet.dewies import dewies_to_lbc\nfrom lbry.wallet.account import Account, AddressManager, SingleKey\nfrom lbry.wallet.network import Network\nfrom lbry.wallet.transaction import Transaction, Output\nfrom lbry.wallet.header import Headers, UnvalidatedHeaders\nfrom lbry.wallet.checkpoints import HASHES\nfrom lbry.wallet.constants import TXO_TYPES, CLAIM_TYPES, COIN, NULL_HASH32\nfrom lbry.wallet.bip32 import PublicKey, PrivateKey\nfrom lbry.wallet.coinselection import CoinSelector\n\nlog = logging.getLogger(__name__)\n\nLedgerType = Type['BaseLedger']\n\n\nclass LedgerRegistry(type):\n\n    ledgers: Dict[str, LedgerType] = {}\n\n    def __new__(mcs, name, bases, attrs):\n        cls: LedgerType = super().__new__(mcs, name, bases, attrs)\n        if not (name == 'BaseLedger' and not bases):\n            ledger_id = cls.get_id()\n            assert ledger_id not in mcs.ledgers, \\\n                f'Ledger with id \"{ledger_id}\" already registered.'\n            mcs.ledgers[ledger_id] = cls\n        return cls\n\n    @classmethod\n    def get_ledger_class(mcs, ledger_id: str) -> LedgerType:\n        return mcs.ledgers[ledger_id]\n\n\nclass TransactionEvent(NamedTuple):\n    address: str\n    tx: Transaction\n\n\nclass AddressesGeneratedEvent(NamedTuple):\n    address_manager: AddressManager\n    addresses: List[str]\n\n\nclass BlockHeightEvent(NamedTuple):\n    height: int\n    change: int\n\n\nclass TransactionCacheItem:\n    __slots__ = '_tx', 'lock', 'has_tx', 'pending_verifications'\n\n    def __init__(self, tx: Optional[Transaction] = None, lock: Optional[asyncio.Lock] = None):\n        self.has_tx = asyncio.Event()\n        self.lock = lock or asyncio.Lock()\n        self._tx = self.tx = tx\n        self.pending_verifications = 0\n\n    @property\n    def tx(self) -> Optional[Transaction]:\n        return self._tx\n\n    @tx.setter\n    def tx(self, tx: Transaction):\n        self._tx = tx\n        if tx is not None:\n            self.has_tx.set()\n\n\nclass Ledger(metaclass=LedgerRegistry):\n    name = 'LBRY Credits'\n    symbol = 'LBC'\n    network_name = 'mainnet'\n\n    headers_class = Headers\n\n    secret_prefix = bytes((0x1c,))\n    pubkey_address_prefix = bytes((0x55,))\n    script_address_prefix = bytes((0x7a,))\n    extended_public_key_prefix = unhexlify('0488b21e')\n    extended_private_key_prefix = unhexlify('0488ade4')\n\n    max_target = 0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n    genesis_hash = '9c89283ba0f3227f6c03b70216b9f665f0118d5e0fa729cedf4fb34d6a34f463'\n    genesis_bits = 0x1f00ffff\n    target_timespan = 150\n\n    default_fee_per_byte = 50\n    default_fee_per_name_char = 0\n\n    checkpoints = HASHES\n\n    def __init__(self, config=None):\n        self.config = config or {}\n        self.db: Database = self.config.get('db') or Database(\n            os.path.join(self.path, \"blockchain.db\")\n        )\n        self.db.ledger = self\n        self.headers: Headers = self.config.get('headers') or self.headers_class(\n            os.path.join(self.path, \"headers\")\n        )\n        self.headers.checkpoints = self.checkpoints\n        self.network: Network = self.config.get('network') or Network(self)\n        self.network.on_header.listen(self.receive_header)\n        self.network.on_status.listen(self.process_status_update)\n\n        self.accounts = []\n        self.fee_per_byte: int = self.config.get('fee_per_byte', self.default_fee_per_byte)\n\n        self._on_transaction_controller = StreamController()\n        self.on_transaction = self._on_transaction_controller.stream\n        self.on_transaction.listen(\n            lambda e: log.info(\n                '(%s) on_transaction: address=%s, height=%s, is_verified=%s, tx.id=%s',\n                self.get_id(), e.address, e.tx.height, e.tx.is_verified, e.tx.id\n            )\n        )\n\n        self._on_address_controller = StreamController()\n        self.on_address = self._on_address_controller.stream\n        self.on_address.listen(\n            lambda e: log.info('(%s) on_address: %s', self.get_id(), e.addresses)\n        )\n\n        self._on_header_controller = StreamController()\n        self.on_header = self._on_header_controller.stream\n        self.on_header.listen(\n            lambda change: log.info(\n                '%s: added %s header blocks, final height %s',\n                self.get_id(), change, self.headers.height\n            )\n        )\n        self._download_height = 0\n\n        self._on_ready_controller = StreamController()\n        self.on_ready = self._on_ready_controller.stream\n\n        self._tx_cache = LRUCacheWithMetrics(self.config.get(\"tx_cache_size\", 1024), metric_name='tx')\n        self._update_tasks = TaskGroup()\n        self._other_tasks = TaskGroup()  # that we dont need to start\n        self._utxo_reservation_lock = asyncio.Lock()\n        self._header_processing_lock = asyncio.Lock()\n        self._address_update_locks: DefaultDict[str, asyncio.Lock] = defaultdict(asyncio.Lock)\n        self._history_lock = asyncio.Lock()\n\n        self.coin_selection_strategy = None\n        self._known_addresses_out_of_sync = set()\n\n        self.fee_per_name_char = self.config.get('fee_per_name_char', self.default_fee_per_name_char)\n        self._balance_cache = LRUCacheWithMetrics(2 ** 15)\n\n    @classmethod\n    def get_id(cls):\n        return '{}_{}'.format(cls.symbol.lower(), cls.network_name.lower())\n\n    @classmethod\n    def hash160_to_address(cls, h160):\n        raw_address = cls.pubkey_address_prefix + h160\n        return Base58.encode(bytearray(raw_address + double_sha256(raw_address)[0:4]))\n\n    @classmethod\n    def hash160_to_script_address(cls, h160):\n        raw_address = cls.script_address_prefix + h160\n        return Base58.encode(bytearray(raw_address + double_sha256(raw_address)[0:4]))\n\n    @staticmethod\n    def address_to_hash160(address):\n        return Base58.decode(address)[1:21]\n\n    @classmethod\n    def is_pubkey_address(cls, address):\n        decoded = Base58.decode_check(address)\n        return decoded[0] == cls.pubkey_address_prefix[0]\n\n    @classmethod\n    def is_script_address(cls, address):\n        decoded = Base58.decode_check(address)\n        return decoded[0] == cls.script_address_prefix[0]\n\n    @classmethod\n    def public_key_to_address(cls, public_key):\n        return cls.hash160_to_address(hash160(public_key))\n\n    @staticmethod\n    def private_key_to_wif(private_key):\n        return b'\\x1c' + private_key + b'\\x01'\n\n    @property\n    def path(self):\n        return os.path.join(self.config['data_path'], self.get_id())\n\n    def add_account(self, account: Account):\n        self.accounts.append(account)\n\n    async def _get_account_and_address_info_for_address(self, wallet, address):\n        match = await self.db.get_address(accounts=wallet.accounts, address=address)\n        if match:\n            for account in wallet.accounts:\n                if match['account'] == account.public_key.address:\n                    return account, match\n\n    async def get_private_key_for_address(self, wallet, address) -> Optional[PrivateKey]:\n        match = await self._get_account_and_address_info_for_address(wallet, address)\n        if match:\n            account, address_info = match\n            return account.get_private_key(address_info['chain'], address_info['pubkey'].n)\n        return None\n\n    async def get_public_key_for_address(self, wallet, address) -> Optional[PublicKey]:\n        match = await self._get_account_and_address_info_for_address(wallet, address)\n        if match:\n            _, address_info = match\n            return address_info['pubkey']\n        return None\n\n    async def get_account_for_address(self, wallet, address):\n        match = await self._get_account_and_address_info_for_address(wallet, address)\n        if match:\n            return match[0]\n\n    async def get_effective_amount_estimators(self, funding_accounts: Iterable[Account]):\n        estimators = []\n        for account in funding_accounts:\n            utxos = await account.get_utxos(no_tx=True, no_channel_info=True)\n            for utxo in utxos:\n                estimators.append(utxo.get_estimator(self))\n        return estimators\n\n    async def get_addresses(self, **constraints):\n        return await self.db.get_addresses(**constraints)\n\n    def get_address_count(self, **constraints):\n        return self.db.get_address_count(**constraints)\n\n    async def get_spendable_utxos(self, amount: int, funding_accounts: Optional[Iterable['Account']], min_amount=1):\n        min_amount = min(amount // 10, min_amount)\n        fee = Output.pay_pubkey_hash(COIN, NULL_HASH32).get_fee(self)\n        selector = CoinSelector(amount, fee)\n        async with self._utxo_reservation_lock:\n            if self.coin_selection_strategy == 'sqlite':\n                return await self.db.get_spendable_utxos(self, amount + fee, funding_accounts, min_amount=min_amount,\n                                                         fee_per_byte=self.fee_per_byte)\n            txos = await self.get_effective_amount_estimators(funding_accounts)\n            spendables = selector.select(txos, self.coin_selection_strategy)\n            if spendables:\n                await self.reserve_outputs(s.txo for s in spendables)\n            return spendables\n\n    def reserve_outputs(self, txos):\n        return self.db.reserve_outputs(txos)\n\n    def release_outputs(self, txos):\n        return self.db.release_outputs(txos)\n\n    def release_tx(self, tx):\n        return self.release_outputs([txi.txo_ref.txo for txi in tx.inputs])\n\n    def get_utxos(self, **constraints):\n        self.constraint_spending_utxos(constraints)\n        return self.db.get_utxos(**constraints)\n\n    def get_utxo_count(self, **constraints):\n        self.constraint_spending_utxos(constraints)\n        return self.db.get_utxo_count(**constraints)\n\n    async def get_txos(self, resolve=False, **constraints) -> List[Output]:\n        txos = await self.db.get_txos(**constraints)\n        if resolve:\n            return await self._resolve_for_local_results(constraints.get('accounts', []), txos)\n        return txos\n\n    def get_txo_count(self, **constraints):\n        return self.db.get_txo_count(**constraints)\n\n    def get_txo_sum(self, **constraints):\n        return self.db.get_txo_sum(**constraints)\n\n    def get_txo_plot(self, **constraints):\n        return self.db.get_txo_plot(**constraints)\n\n    def get_transactions(self, **constraints):\n        return self.db.get_transactions(**constraints)\n\n    def get_transaction_count(self, **constraints):\n        return self.db.get_transaction_count(**constraints)\n\n    async def get_local_status_and_history(self, address, history=None):\n        if not history:\n            address_details = await self.db.get_address(address=address)\n            history = (address_details['history'] if address_details else '') or ''\n        parts = history.split(':')[:-1]\n        return (\n            hexlify(sha256(history.encode())).decode() if history else None,\n            list(zip(parts[0::2], map(int, parts[1::2])))\n        )\n\n    @staticmethod\n    def get_root_of_merkle_tree(branches, branch_positions, working_branch):\n        for i, branch in enumerate(branches):\n            other_branch = unhexlify(branch)[::-1]\n            other_branch_on_left = bool((branch_positions >> i) & 1)\n            if other_branch_on_left:\n                combined = other_branch + working_branch\n            else:\n                combined = working_branch + other_branch\n            working_branch = double_sha256(combined)\n        return hexlify(working_branch[::-1])\n\n    async def start(self):\n        if not os.path.exists(self.path):\n            os.mkdir(self.path)\n        await asyncio.wait(map(asyncio.create_task, [\n            self.db.open(),\n            self.headers.open()\n        ]))\n        fully_synced = self.on_ready.first\n        asyncio.create_task(self.network.start())\n        await self.network.on_connected.first\n        async with self._header_processing_lock:\n            await self._update_tasks.add(self.initial_headers_sync())\n        self.network.on_connected.listen(self.join_network)\n        asyncio.ensure_future(self.join_network())\n        await fully_synced\n        await self.db.release_all_outputs()\n        await asyncio.gather(*(a.maybe_migrate_certificates() for a in self.accounts))\n        await asyncio.gather(*(a.save_max_gap() for a in self.accounts))\n        if len(self.accounts) > 10:\n            log.info(\"Loaded %i accounts\", len(self.accounts))\n        else:\n            await self._report_state()\n        self.on_transaction.listen(self._reset_balance_cache)\n\n    async def join_network(self, *_):\n        log.info(\"Subscribing and updating accounts.\")\n        await self._update_tasks.add(self.subscribe_accounts())\n        await self._update_tasks.done.wait()\n        self._on_ready_controller.add(True)\n\n    async def stop(self):\n        self._update_tasks.cancel()\n        self._other_tasks.cancel()\n        await self._update_tasks.done.wait()\n        await self._other_tasks.done.wait()\n        await self.network.stop()\n        await self.db.close()\n        await self.headers.close()\n\n    async def tasks_are_done(self):\n        await self._update_tasks.done.wait()\n        await self._other_tasks.done.wait()\n\n    @property\n    def local_height_including_downloaded_height(self):\n        return max(self.headers.height, self._download_height)\n\n    async def initial_headers_sync(self):\n        get_chunk = partial(self.network.retriable_call, self.network.get_headers, count=1000, b64=True)\n        self.headers.chunk_getter = get_chunk\n\n        async def doit():\n            for height in reversed(sorted(self.headers.known_missing_checkpointed_chunks)):\n                async with self._header_processing_lock:\n                    await self.headers.ensure_chunk_at(height)\n        self._other_tasks.add(doit())\n        await self.update_headers()\n\n    async def update_headers(self, height=None, headers=None, subscription_update=False):\n        rewound = 0\n        while True:\n\n            if height is None or height > len(self.headers):\n                # sometimes header subscription updates are for a header in the future\n                # which can't be connected, so we do a normal header sync instead\n                height = len(self.headers)\n                headers = None\n                subscription_update = False\n\n            if not headers:\n                header_response = await self.network.retriable_call(self.network.get_headers, height, 2001)\n                headers = header_response['hex']\n\n            if not headers:\n                # Nothing to do, network thinks we're already at the latest height.\n                return\n\n            added = await self.headers.connect(height, unhexlify(headers))\n            if added > 0:\n                height += added\n                self._on_header_controller.add(\n                    BlockHeightEvent(self.headers.height, added))\n\n                if rewound > 0:\n                    # we started rewinding blocks and apparently found\n                    # a new chain\n                    rewound = 0\n                    await self.db.rewind_blockchain(height)\n\n                if subscription_update:\n                    # subscription updates are for latest header already\n                    # so we don't need to check if there are newer / more\n                    # on another loop of update_headers(), just return instead\n                    return\n\n            elif added == 0:\n                # we had headers to connect but none got connected, probably a reorganization\n                height -= 1\n                rewound += 1\n                log.warning(\n                    \"Blockchain Reorganization: attempting rewind to height %s from starting height %s\",\n                    height, height+rewound\n                )\n                self._tx_cache.clear()\n\n            else:\n                raise IndexError(f\"headers.connect() returned negative number ({added})\")\n\n            if height < 0:\n                raise IndexError(\n                    \"Blockchain reorganization rewound all the way back to genesis hash. \"\n                    \"Something is very wrong. Maybe you are on the wrong blockchain?\"\n                )\n\n            if rewound >= 100:\n                raise IndexError(\n                    \"Blockchain reorganization dropped {} headers. This is highly unusual. \"\n                    \"Will not continue to attempt reorganizing. Please, delete the ledger \"\n                    \"synchronization directory inside your wallet directory (folder: '{}') and \"\n                    \"restart the program to synchronize from scratch.\"\n                    .format(rewound, self.get_id())\n                )\n\n            headers = None  # ready to download some more headers\n\n            # if we made it this far and this was a subscription_update\n            # it means something went wrong and now we're doing a more\n            # robust sync, turn off subscription update shortcut\n            subscription_update = False\n\n    async def receive_header(self, response):\n        async with self._header_processing_lock:\n            header = response[0]\n            await self.update_headers(\n                height=header['height'], headers=header['hex'], subscription_update=True\n            )\n\n    async def subscribe_accounts(self):\n        if self.network.is_connected and self.accounts:\n            log.info(\"Subscribe to %i accounts\", len(self.accounts))\n            await asyncio.wait(map(asyncio.create_task, [\n                self.subscribe_account(a) for a in self.accounts\n            ]))\n\n    async def subscribe_account(self, account: Account):\n        for address_manager in account.address_managers.values():\n            await self.subscribe_addresses(address_manager, await address_manager.get_addresses())\n        await account.ensure_address_gap()\n        await account.deterministic_channel_keys.ensure_cache_primed()\n\n    async def unsubscribe_account(self, account: Account):\n        for address in await account.get_addresses():\n            await self.network.unsubscribe_address(address)\n\n    async def announce_addresses(self, address_manager: AddressManager, addresses: List[str]):\n        await self.subscribe_addresses(address_manager, addresses)\n        await self._on_address_controller.add(\n            AddressesGeneratedEvent(address_manager, addresses)\n        )\n\n    async def subscribe_addresses(self, address_manager: AddressManager, addresses: List[str], batch_size: int = 1000):\n        if self.network.is_connected and addresses:\n            addresses_remaining = list(addresses)\n            while addresses_remaining:\n                batch = addresses_remaining[:batch_size]\n                results = await self.network.subscribe_address(*batch)\n                for address, remote_status in zip(batch, results):\n                    self._update_tasks.add(self.update_history(address, remote_status, address_manager))\n                addresses_remaining = addresses_remaining[batch_size:]\n                if self.network.client and self.network.client.server_address_and_port:\n                    log.info(\"subscribed to %i/%i addresses on %s:%i\", len(addresses) - len(addresses_remaining),\n                             len(addresses), *self.network.client.server_address_and_port)\n            if self.network.client and self.network.client.server_address_and_port:\n                log.info(\n                    \"finished subscribing to %i addresses on %s:%i\", len(addresses),\n                    *self.network.client.server_address_and_port\n                )\n\n    def process_status_update(self, update):\n        address, remote_status = update\n        self._update_tasks.add(self.update_history(address, remote_status))\n\n    async def update_history(self, address, remote_status, address_manager: AddressManager = None,\n                             reattempt_update: bool = True):\n        async with self._address_update_locks[address]:\n            self._known_addresses_out_of_sync.discard(address)\n            local_status, local_history = await self.get_local_status_and_history(address)\n\n            if local_status == remote_status:\n                return True\n\n            remote_history = await self.network.retriable_call(self.network.get_history, address)\n            remote_history = list(map(itemgetter('tx_hash', 'height'), remote_history))\n            we_need = set(remote_history) - set(local_history)\n            if not we_need:\n                remote_missing = set(local_history) - set(remote_history)\n                if remote_missing:\n                    log.warning(\n                        \"%i transactions we have for %s are not in the remote address history\",\n                        len(remote_missing), address\n                    )\n                return True\n\n            to_request = {}\n            pending_synced_history = {}\n            already_synced = set()\n\n            already_synced_offset = 0\n            for i, (txid, remote_height) in enumerate(remote_history):\n                if i == already_synced_offset and i < len(local_history) and local_history[i] == (txid, remote_height):\n                    pending_synced_history[i] = f'{txid}:{remote_height}:'\n                    already_synced.add((txid, remote_height))\n                    already_synced_offset += 1\n                    continue\n\n            tx_indexes = {}\n\n            for i, (txid, remote_height) in enumerate(remote_history):\n                tx_indexes[txid] = i\n                if (txid, remote_height) in already_synced:\n                    continue\n                to_request[i] = (txid, remote_height)\n\n            log.debug(\n                \"request %i transactions, %i/%i for %s are already synced\", len(to_request), len(already_synced),\n                len(remote_history), address\n            )\n            remote_history_txids = {txid for txid, _ in remote_history}\n            async for tx in self.request_synced_transactions(to_request, remote_history_txids, address):\n                self.maybe_has_channel_key(tx)\n                pending_synced_history[tx_indexes[tx.id]] = f\"{tx.id}:{tx.height}:\"\n                if len(pending_synced_history) % 100 == 0:\n                    log.info(\"Syncing address %s: %d/%d\", address, len(pending_synced_history), len(to_request))\n            log.info(\"Sync finished for address %s: %d/%d\", address, len(pending_synced_history), len(to_request))\n\n            assert len(pending_synced_history) == len(remote_history), \\\n                f\"{len(pending_synced_history)} vs {len(remote_history)} for {address}\"\n            synced_history = \"\"\n            for remote_i, i in zip(range(len(remote_history)), sorted(pending_synced_history.keys())):\n                assert i == remote_i, f\"{i} vs {remote_i}\"\n                txid, height = remote_history[remote_i]\n                if f\"{txid}:{height}:\" != pending_synced_history[i]:\n                    log.warning(\"history mismatch: %s vs %s\", remote_history[remote_i], pending_synced_history[i])\n                synced_history += pending_synced_history[i]\n            await self.db.set_address_history(address, synced_history)\n\n            if address_manager is None:\n                address_manager = await self.get_address_manager_for_address(address)\n\n            if address_manager is not None:\n                await address_manager.ensure_address_gap()\n\n            local_status, local_history = \\\n                await self.get_local_status_and_history(address, synced_history)\n\n            if local_status != remote_status:\n                if local_history == remote_history:\n                    log.warning(\n                        \"%s has a synced history but a mismatched status\", address\n                    )\n                    return True\n                remote_set = set(remote_history)\n                local_set = set(local_history)\n                log.warning(\n                    \"%s is out of sync after syncing.\\n\"\n                    \"Remote: %s with %d items (%i unique), local: %s with %d items (%i unique).\\n\"\n                    \"Histories are mismatched on %i items.\\n\"\n                    \"Local is missing\\n\"\n                    \"%s\\n\"\n                    \"Remote is missing\\n\"\n                    \"%s\\n\"\n                    \"******\",\n                    address, remote_status, len(remote_history), len(remote_set),\n                    local_status, len(local_history), len(local_set), len(remote_set.symmetric_difference(local_set)),\n                    \"\\n\".join([f\"{txid} - {height}\" for txid, height in local_set.difference(remote_set)]),\n                    \"\\n\".join([f\"{txid} - {height}\" for txid, height in remote_set.difference(local_set)])\n                )\n                self._known_addresses_out_of_sync.add(address)\n                return False\n            else:\n                log.debug(\"finished syncing transaction history for %s, %i known txs\", address, len(local_history))\n                return True\n\n    async def maybe_verify_transaction(self, tx, remote_height, merkle=None):\n        tx.height = remote_height\n        if 0 < remote_height < len(self.headers):\n            # can't be tx.pending_verifications == 1 because we have to handle the transaction_show case\n            if not merkle:\n                merkle = await self.network.retriable_call(self.network.get_merkle, tx.id, remote_height)\n            if 'merkle' not in merkle:\n                return\n            merkle_root = self.get_root_of_merkle_tree(merkle['merkle'], merkle['pos'], tx.hash)\n            header = await self.headers.get(remote_height)\n            tx.position = merkle['pos']\n            tx.is_verified = merkle_root == header['merkle_root']\n        return tx\n\n    def maybe_has_channel_key(self, tx):\n        for txo in tx._outputs:\n            if txo.can_decode_claim and txo.claim.is_channel:\n                for account in self.accounts:\n                    account.deterministic_channel_keys.maybe_generate_deterministic_key_for_channel(txo)\n\n    async def request_transactions(self, to_request: Tuple[Tuple[str, int], ...], cached=False):\n        batches = [[]]\n        remote_heights = {}\n        cache_hits = set()\n\n        for txid, height in sorted(to_request, key=lambda x: x[1]):\n            if cached:\n                cached_tx = self._tx_cache.get(txid)\n                if cached_tx is not None:\n                    if cached_tx.tx is not None and cached_tx.tx.is_verified:\n                        cache_hits.add(txid)\n                        continue\n                else:\n                    self._tx_cache[txid] = TransactionCacheItem()\n            remote_heights[txid] = height\n            if len(batches[-1]) == 100:\n                batches.append([])\n            batches[-1].append(txid)\n        if not batches[-1]:\n            batches.pop()\n        if cached and cache_hits:\n            yield {txid: self._tx_cache[txid].tx for txid in cache_hits}\n\n        for batch in batches:\n            txs = await self._single_batch(batch, remote_heights)\n            if cached:\n                for txid, tx in txs.items():\n                    self._tx_cache[txid].tx = tx\n            yield txs\n\n    async def request_synced_transactions(self, to_request, remote_history, address):\n        async for txs in self.request_transactions(((txid, height) for txid, height in to_request.values())):\n            for tx in txs.values():\n                yield tx\n            await self._sync_and_save_batch(address, remote_history, txs)\n\n    async def _single_batch(self, batch, remote_heights):\n        heights = {remote_heights[txid] for txid in batch}\n        unrestriced = 0 < min(heights) < max(heights) < max(self.headers.checkpoints or [0])\n        batch_result = await self.network.retriable_call(self.network.get_transaction_batch, batch, not unrestriced)\n        txs = {}\n        for txid, (raw, merkle) in batch_result.items():\n            remote_height = remote_heights[txid]\n            tx = Transaction(unhexlify(raw), height=remote_height)\n            txs[tx.id] = tx\n            await self.maybe_verify_transaction(tx, remote_height, merkle)\n        return txs\n\n    async def _sync_and_save_batch(self, address, remote_history, pending_txs):\n        await asyncio.gather(*(self._sync(tx, remote_history, pending_txs) for tx in pending_txs.values()))\n        await self.db.save_transaction_io_batch(\n            pending_txs.values(), address, self.address_to_hash160(address), \"\"\n        )\n        while pending_txs:\n            self._on_transaction_controller.add(TransactionEvent(address, pending_txs.popitem()[1]))\n\n    async def _sync(self, tx, remote_history, pending_txs):\n        check_db_for_txos = {}\n        for txi in tx.inputs:\n            if txi.txo_ref.txo is not None:\n                continue\n            wanted_txid = txi.txo_ref.tx_ref.id\n            if wanted_txid not in remote_history:\n                continue\n            if wanted_txid in pending_txs:\n                txi.txo_ref = pending_txs[wanted_txid].outputs[txi.txo_ref.position].ref\n            else:\n                check_db_for_txos[txi] = txi.txo_ref.id\n\n        referenced_txos = {} if not check_db_for_txos else {\n            txo.id: txo for txo in await self.db.get_txos(\n                txoid__in=list(check_db_for_txos.values()), order_by='txo.txoid', no_tx=True\n            )\n        }\n\n        for txi in check_db_for_txos:\n            if txi.txo_ref.id in referenced_txos:\n                txi.txo_ref = referenced_txos[txi.txo_ref.id].ref\n            else:\n                tx_from_db = await self.db.get_transaction(txid=txi.txo_ref.tx_ref.id)\n                if tx_from_db is None:\n                    log.warning(\"%s not on db, not on cache, but on remote history!\", txi.txo_ref.id)\n                else:\n                    txi.txo_ref = tx_from_db.outputs[txi.txo_ref.position].ref\n        return tx\n\n    async def get_address_manager_for_address(self, address) -> Optional[AddressManager]:\n        details = await self.db.get_address(address=address)\n        for account in self.accounts:\n            if account.id == details['account']:\n                return account.address_managers[details['chain']]\n        return None\n\n    async def broadcast_or_release(self, tx, blocking=False):\n        try:\n            await self.broadcast(tx)\n        except:\n            await self.release_tx(tx)\n            raise\n        if blocking:\n            await self.wait(tx, timeout=None)\n\n    def broadcast(self, tx):\n        # broadcast can't be a retriable call yet\n        return self.network.broadcast(hexlify(tx.raw).decode())\n\n    async def wait(self, tx: Transaction, height=-1, timeout=1):\n        timeout = timeout or 600  # after 10 minutes there is almost 0 hope\n        addresses = set()\n        for txi in tx.inputs:\n            if txi.txo_ref.txo is not None:\n                addresses.add(\n                    self.hash160_to_address(txi.txo_ref.txo.pubkey_hash)\n                )\n        for txo in tx.outputs:\n            if txo.is_pubkey_hash:\n                addresses.add(self.hash160_to_address(txo.pubkey_hash))\n            elif txo.is_script_hash:\n                addresses.add(self.hash160_to_script_address(txo.script_hash))\n        start = int(time.perf_counter())\n        while timeout and (int(time.perf_counter()) - start) <= timeout:\n            if await self._wait_round(tx, height, addresses):\n                return\n        raise asyncio.TimeoutError(f'Timed out waiting for transaction. {tx.id}')\n\n    async def _wait_round(self, tx: Transaction, height: int, addresses: Iterable[str]):\n        records = await self.db.get_addresses(address__in=addresses)\n        _, pending = await asyncio.wait([\n            self.on_transaction.where(partial(\n                lambda a, e: a == e.address and e.tx.height >= height and e.tx.id == tx.id,\n                address_record['address']\n            )) for address_record in records\n        ], timeout=1)\n        if not pending:\n            return True\n        records = await self.db.get_addresses(address__in=addresses)\n        for record in records:\n            local_history = (await self.get_local_status_and_history(\n                record['address'], history=record['history']\n            ))[1] if record['history'] else []\n            for txid, local_height in local_history:\n                if txid == tx.id:\n                    if local_height >= height or (local_height == 0 and height > local_height):\n                        return True\n                    log.warning(\n                        \"local history has higher height than remote for %s (%i vs %i)\", txid,\n                        local_height, height\n                    )\n                    return False\n            log.warning(\n                \"local history does not contain %s, requested height %i\", tx.id, height\n            )\n        return False\n\n    async def _inflate_outputs(\n            self, query, accounts,\n            include_purchase_receipt=False,\n            include_is_my_output=False,\n            include_sent_supports=False,\n            include_sent_tips=False,\n            include_received_tips=False) -> Tuple[List[Output], dict, int, int]:\n        encoded_outputs = await query\n        outputs = Outputs.from_base64(encoded_outputs or '')  # TODO: why is the server returning None?\n        txs: List[Transaction] = []\n        if len(outputs.txs) > 0:\n            async for tx in self.request_transactions(tuple(outputs.txs), cached=True):\n                txs.extend(tx.values())\n\n        _txos, blocked = outputs.inflate(txs)\n\n        txos = []\n        for txo in _txos:\n            if isinstance(txo, Output):\n                # transactions and outputs are cached and shared between wallets\n                # we don't want to leak informaion between wallet so we add the\n                # wallet specific metadata on throw away copies of the txos\n                txo = copy.copy(txo)\n                channel = txo.channel\n                txo.purchase_receipt = None\n                txo.update_annotations(None)\n                txo.channel = channel\n            txos.append(txo)\n\n        includes = (\n            include_purchase_receipt, include_is_my_output,\n            include_sent_supports, include_sent_tips\n        )\n        if accounts and any(includes):\n            receipts = {}\n            if include_purchase_receipt:\n                priced_claims = []\n                for txo in txos:\n                    if isinstance(txo, Output) and txo.has_price:\n                        priced_claims.append(txo)\n                if priced_claims:\n                    receipts = {\n                        txo.purchased_claim_id: txo for txo in\n                        await self.db.get_purchases(\n                            accounts=accounts,\n                            purchased_claim_id__in=[c.claim_id for c in priced_claims]\n                        )\n                    }\n            for txo in txos:\n                if isinstance(txo, Output) and txo.can_decode_claim:\n                    if include_purchase_receipt:\n                        txo.purchase_receipt = receipts.get(txo.claim_id)\n                    if include_is_my_output:\n                        mine = await self.db.get_txo_count(\n                            claim_id=txo.claim_id, txo_type__in=CLAIM_TYPES, is_my_output=True,\n                            is_spent=False, accounts=accounts\n                        )\n                        if mine:\n                            txo.is_my_output = True\n                        else:\n                            txo.is_my_output = False\n                    if include_sent_supports:\n                        supports = await self.db.get_txo_sum(\n                            claim_id=txo.claim_id, txo_type=TXO_TYPES['support'],\n                            is_my_input=True, is_my_output=True,\n                            is_spent=False, accounts=accounts\n                        )\n                        txo.sent_supports = supports\n                    if include_sent_tips:\n                        tips = await self.db.get_txo_sum(\n                            claim_id=txo.claim_id, txo_type=TXO_TYPES['support'],\n                            is_my_input=True, is_my_output=False,\n                            accounts=accounts\n                        )\n                        txo.sent_tips = tips\n                    if include_received_tips:\n                        tips = await self.db.get_txo_sum(\n                            claim_id=txo.claim_id, txo_type=TXO_TYPES['support'],\n                            is_my_input=False, is_my_output=True,\n                            accounts=accounts\n                        )\n                        txo.received_tips = tips\n        return txos, blocked, outputs.offset, outputs.total\n\n    async def resolve(self, accounts, urls, **kwargs):\n        txos = []\n        urls_copy = list(urls)\n        resolve = partial(self.network.retriable_call, self.network.resolve)\n        while urls_copy:\n            batch, urls_copy = urls_copy[:100], urls_copy[100:]\n            txos.extend(\n                (await self._inflate_outputs(\n                    resolve(batch), accounts, **kwargs\n                ))[0]\n            )\n\n        assert len(urls) == len(txos), \"Mismatch between urls requested for resolve and responses received.\"\n        result = {}\n        for url, txo in zip(urls, txos):\n            if txo:\n                if isinstance(txo, Output) and URL.parse(url).has_stream_in_channel:\n                    if not txo.channel or not txo.is_signed_by(txo.channel, self):\n                        txo = {'error': {'name': INVALID, 'text': f'{url} has invalid channel signature'}}\n            else:\n                txo = {'error': {'name': NOT_FOUND, 'text': f'{url} did not resolve to a claim'}}\n            result[url] = txo\n        return result\n\n    async def sum_supports(self, new_sdk_server, **kwargs) -> List[Dict]:\n        return await self.network.sum_supports(new_sdk_server, **kwargs)\n\n    async def claim_search(\n            self, accounts,\n            include_purchase_receipt=False,\n            include_is_my_output=False,\n            **kwargs) -> Tuple[List[Output], dict, int, int]:\n        return await self._inflate_outputs(\n            self.network.claim_search(**kwargs), accounts,\n            include_purchase_receipt=include_purchase_receipt,\n            include_is_my_output=include_is_my_output\n        )\n\n    # async def get_claim_by_claim_id(self, accounts, claim_id, **kwargs) -> Output:\n    #     return await self.network.get_claim_by_id(claim_id)\n\n    async def get_claim_by_claim_id(self, claim_id, accounts=None, include_purchase_receipt=False,\n                                    include_is_my_output=False):\n        accounts = accounts or []\n        # return await self.network.get_claim_by_id(claim_id)\n        inflated = await self._inflate_outputs(\n            self.network.get_claim_by_id(claim_id), accounts,\n            include_purchase_receipt=include_purchase_receipt,\n            include_is_my_output=include_is_my_output,\n        )\n        txos = inflated[0]\n        if txos:\n            return txos[0]\n\n    async def _report_state(self):\n        try:\n            for account in self.accounts:\n                balance = dewies_to_lbc(await account.get_balance(include_claims=True))\n                channel_count = await account.get_channel_count()\n                claim_count = await account.get_claim_count()\n                if isinstance(account.receiving, SingleKey):\n                    log.info(\"Loaded single key account %s with %s LBC. \"\n                             \"%d channels, %d certificates and %d claims\",\n                             account.id, balance, channel_count, len(account.channel_keys), claim_count)\n                else:\n                    total_receiving = len(await account.receiving.get_addresses())\n                    total_change = len(await account.change.get_addresses())\n                    log.info(\"Loaded account %s with %s LBC, %d receiving addresses (gap: %d), \"\n                             \"%d change addresses (gap: %d), %d channels, %d certificates and %d claims. \",\n                             account.id, balance, total_receiving, account.receiving.gap, total_change,\n                             account.change.gap, channel_count, len(account.channel_keys), claim_count)\n        except Exception:\n            log.exception(\n                'Failed to display wallet state, please file issue '\n                'for this bug along with the traceback you see below:')\n\n    async def _reset_balance_cache(self, e: TransactionEvent):\n        account_ids = [\n            r['account'] for r in await self.db.get_addresses(('account',), address=e.address)\n        ]\n        for account_id in account_ids:\n            if account_id in self._balance_cache:\n                del self._balance_cache[account_id]\n\n    @staticmethod\n    def constraint_spending_utxos(constraints):\n        constraints['txo_type__in'] = (0, TXO_TYPES['purchase'])\n\n    async def get_purchases(self, resolve=False, **constraints):\n        purchases = await self.db.get_purchases(**constraints)\n        if resolve:\n            claim_ids = [p.purchased_claim_id for p in purchases]\n            try:\n                resolved, _, _, _ = await self.claim_search([], claim_ids=claim_ids)\n            except Exception:\n                log.exception(\"Resolve failed while looking up purchased claim ids:\")\n                resolved = []\n            lookup = {claim.claim_id: claim for claim in resolved}\n            for purchase in purchases:\n                purchase.purchased_claim = lookup.get(purchase.purchased_claim_id)\n        return purchases\n\n    def get_purchase_count(self, resolve=False, **constraints):\n        return self.db.get_purchase_count(**constraints)\n\n    async def _resolve_for_local_results(self, accounts, txos):\n        txos = await self._resolve_for_local_claim_results(accounts, txos)\n        txos = await self._resolve_for_local_support_results(accounts, txos)\n        return txos\n\n    async def _resolve_for_local_claim_results(self, accounts, txos):\n        results = []\n        response = await self.resolve(\n            accounts, [txo.permanent_url for txo in txos if txo.can_decode_claim]\n        )\n        for txo in txos:\n            resolved = response.get(txo.permanent_url) if txo.can_decode_claim else None\n            if isinstance(resolved, Output):\n                resolved.update_annotations(txo)\n                results.append(resolved)\n            else:\n                if isinstance(resolved, dict) and 'error' in resolved:\n                    txo.meta['error'] = resolved['error']\n                results.append(txo)\n        return results\n\n    async def _resolve_for_local_support_results(self, accounts, txos):\n        channel_ids = set()\n        signed_support_txos = []\n        for txo in txos:\n            support = txo.can_decode_support\n            if support and support.signing_channel_id:\n                channel_ids.add(support.signing_channel_id)\n                signed_support_txos.append(txo)\n        if channel_ids:\n            channels = {\n                channel.claim_id: channel for channel in\n                (await self.claim_search(accounts, claim_ids=list(channel_ids)))[0]\n            }\n            for txo in signed_support_txos:\n                txo.channel = channels.get(txo.support.signing_channel_id)\n        return txos\n\n    async def get_claims(self, resolve=False, **constraints):\n        claims = await self.db.get_claims(**constraints)\n        if resolve:\n            return await self._resolve_for_local_results(constraints.get('accounts', []), claims)\n        return claims\n\n    def get_claim_count(self, **constraints):\n        return self.db.get_claim_count(**constraints)\n\n    async def get_streams(self, resolve=False, **constraints):\n        streams = await self.db.get_streams(**constraints)\n        if resolve:\n            return await self._resolve_for_local_results(constraints.get('accounts', []), streams)\n        return streams\n\n    def get_stream_count(self, **constraints):\n        return self.db.get_stream_count(**constraints)\n\n    async def get_channels(self, resolve=False, **constraints):\n        channels = await self.db.get_channels(**constraints)\n        if resolve:\n            return await self._resolve_for_local_results(constraints.get('accounts', []), channels)\n        return channels\n\n    def get_channel_count(self, **constraints):\n        return self.db.get_channel_count(**constraints)\n\n    async def resolve_collection(self, collection, offset=0, page_size=1):\n        claim_ids = collection.claim.collection.claims.ids[offset:page_size + offset]\n        try:\n            resolve_results, _, _, _ = await self.claim_search([], claim_ids=claim_ids)\n        except Exception:\n            log.exception(\"Resolve failed while looking up collection claim ids:\")\n            return []\n        claims = []\n        for claim_id in claim_ids:\n            found = False\n            for txo in resolve_results:\n                if txo.claim_id == claim_id:\n                    claims.append(txo)\n                    found = True\n                    break\n            if not found:\n                claims.append(None)\n        return claims\n\n    async def get_collections(self, resolve_claims=0, resolve=False, **constraints):\n        collections = await self.db.get_collections(**constraints)\n        if resolve:\n            collections = await self._resolve_for_local_results(constraints.get('accounts', []), collections)\n        if resolve_claims > 0:\n            for collection in collections:\n                collection.claims = await self.resolve_collection(collection, page_size=resolve_claims)\n        return collections\n\n    def get_collection_count(self, resolve_claims=0, **constraints):\n        return self.db.get_collection_count(**constraints)\n\n    def get_supports(self, **constraints):\n        return self.db.get_supports(**constraints)\n\n    def get_support_count(self, **constraints):\n        return self.db.get_support_count(**constraints)\n\n    async def get_transaction_history(self, read_only=False, **constraints):\n        txs: List[Transaction] = await self.db.get_transactions(\n            include_is_my_output=True, include_is_spent=True,\n            read_only=read_only, **constraints\n        )\n        headers = self.headers\n        history = []\n        for tx in txs:  # pylint: disable=too-many-nested-blocks\n            ts = headers.estimated_timestamp(tx.height)\n            item = {\n                'txid': tx.id,\n                'timestamp': ts,\n                'date': datetime.fromtimestamp(ts).isoformat(' ')[:-3] if tx.height > 0 else None,\n                'confirmations': (headers.height + 1) - tx.height if tx.height > 0 else 0,\n                'claim_info': [],\n                'update_info': [],\n                'support_info': [],\n                'abandon_info': [],\n                'purchase_info': []\n            }\n            is_my_inputs = all(txi.is_my_input for txi in tx.inputs)\n            if is_my_inputs:\n                # fees only matter if we are the ones paying them\n                item['value'] = dewies_to_lbc(tx.net_account_balance + tx.fee)\n                item['fee'] = dewies_to_lbc(-tx.fee)\n            else:\n                # someone else paid the fees\n                item['value'] = dewies_to_lbc(tx.net_account_balance)\n                item['fee'] = '0.0'\n            for txo in tx.my_claim_outputs:\n                item['claim_info'].append({\n                    'address': txo.get_address(self),\n                    'balance_delta': dewies_to_lbc(-txo.amount),\n                    'amount': dewies_to_lbc(txo.amount),\n                    'claim_id': txo.claim_id,\n                    'claim_name': txo.claim_name,\n                    'nout': txo.position,\n                    'is_spent': txo.is_spent,\n                })\n            for txo in tx.my_update_outputs:\n                if is_my_inputs:  # updating my own claim\n                    previous = None\n                    for txi in tx.inputs:\n                        if txi.txo_ref.txo is not None:\n                            other_txo = txi.txo_ref.txo\n                            if (other_txo.is_claim or other_txo.script.is_support_claim) \\\n                                and other_txo.claim_id == txo.claim_id:\n                                previous = other_txo\n                                break\n                    if previous is not None:\n                        item['update_info'].append({\n                            'address': txo.get_address(self),\n                            'balance_delta': dewies_to_lbc(previous.amount - txo.amount),\n                            'amount': dewies_to_lbc(txo.amount),\n                            'claim_id': txo.claim_id,\n                            'claim_name': txo.claim_name,\n                            'nout': txo.position,\n                            'is_spent': txo.is_spent,\n                        })\n                else:  # someone sent us their claim\n                    item['update_info'].append({\n                        'address': txo.get_address(self),\n                        'balance_delta': dewies_to_lbc(0),\n                        'amount': dewies_to_lbc(txo.amount),\n                        'claim_id': txo.claim_id,\n                        'claim_name': txo.claim_name,\n                        'nout': txo.position,\n                        'is_spent': txo.is_spent,\n                    })\n            for txo in tx.my_support_outputs:\n                item['support_info'].append({\n                    'address': txo.get_address(self),\n                    'balance_delta': dewies_to_lbc(txo.amount if not is_my_inputs else -txo.amount),\n                    'amount': dewies_to_lbc(txo.amount),\n                    'claim_id': txo.claim_id,\n                    'claim_name': txo.claim_name,\n                    'is_tip': not is_my_inputs,\n                    'nout': txo.position,\n                    'is_spent': txo.is_spent,\n                })\n            if is_my_inputs:\n                for txo in tx.other_support_outputs:\n                    item['support_info'].append({\n                        'address': txo.get_address(self),\n                        'balance_delta': dewies_to_lbc(-txo.amount),\n                        'amount': dewies_to_lbc(txo.amount),\n                        'claim_id': txo.claim_id,\n                        'claim_name': txo.claim_name,\n                        'is_tip': is_my_inputs,\n                        'nout': txo.position,\n                        'is_spent': txo.is_spent,\n                    })\n            for txo in tx.my_abandon_outputs:\n                item['abandon_info'].append({\n                    'address': txo.get_address(self),\n                    'balance_delta': dewies_to_lbc(txo.amount),\n                    'amount': dewies_to_lbc(txo.amount),\n                    'claim_id': txo.claim_id,\n                    'claim_name': txo.claim_name,\n                    'nout': txo.position\n                })\n            for txo in tx.any_purchase_outputs:\n                item['purchase_info'].append({\n                    'address': txo.get_address(self),\n                    'balance_delta': dewies_to_lbc(txo.amount if not is_my_inputs else -txo.amount),\n                    'amount': dewies_to_lbc(txo.amount),\n                    'claim_id': txo.purchased_claim_id,\n                    'nout': txo.position,\n                    'is_spent': txo.is_spent,\n                })\n            history.append(item)\n        return history\n\n    def get_transaction_history_count(self, read_only=False, **constraints):\n        return self.db.get_transaction_count(read_only=read_only, **constraints)\n\n    async def get_detailed_balance(self, accounts, confirmations=0):\n        result = {\n            'total': 0,\n            'available': 0,\n            'reserved': 0,\n            'reserved_subtotals': {\n                'claims': 0,\n                'supports': 0,\n                'tips': 0\n            }\n        }\n        for account in accounts:\n            balance = self._balance_cache.get(account.id)\n            if not balance:\n                balance = self._balance_cache[account.id] = \\\n                    await account.get_detailed_balance(confirmations)\n            for key, value in balance.items():\n                if key == 'reserved_subtotals':\n                    for subkey, subvalue in value.items():\n                        result['reserved_subtotals'][subkey] += subvalue\n                else:\n                    result[key] += value\n        return result\n\n\nclass TestNetLedger(Ledger):\n    network_name = 'testnet'\n    pubkey_address_prefix = bytes((111,))\n    script_address_prefix = bytes((196,))\n    extended_public_key_prefix = unhexlify('043587cf')\n    extended_private_key_prefix = unhexlify('04358394')\n    checkpoints = {}\n\n\nclass RegTestLedger(Ledger):\n    network_name = 'regtest'\n    headers_class = UnvalidatedHeaders\n    pubkey_address_prefix = bytes((111,))\n    script_address_prefix = bytes((196,))\n    extended_public_key_prefix = unhexlify('043587cf')\n    extended_private_key_prefix = unhexlify('04358394')\n\n    max_target = 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n    genesis_hash = '6e3fcf1299d4ec5d79c3a4c91d624a4acf9e2e173d95a1a0504f677669687556'\n    genesis_bits = 0x207fffff\n    target_timespan = 1\n    checkpoints = {}\n"
  },
  {
    "path": "lbry/wallet/manager.py",
    "content": "import os\nimport json\nimport typing\nimport logging\nimport asyncio\n\nfrom binascii import unhexlify\nfrom decimal import Decimal\nfrom typing import List, Type, MutableSequence, MutableMapping, Optional\n\nfrom lbry.error import KeyFeeAboveMaxAllowedError, WalletNotLoadedError\nfrom lbry.conf import Config, NOT_SET\n\nfrom lbry.wallet.dewies import dewies_to_lbc\nfrom lbry.wallet.account import Account\nfrom lbry.wallet.ledger import Ledger, LedgerRegistry\nfrom lbry.wallet.transaction import Transaction, Output\nfrom lbry.wallet.database import Database\nfrom lbry.wallet.wallet import Wallet, WalletStorage, ENCRYPT_ON_DISK\nfrom lbry.wallet.rpc.jsonrpc import CodeMessageError\n\nif typing.TYPE_CHECKING:\n    from lbry.extras.daemon.exchange_rate_manager import ExchangeRateManager\n\n\nlog = logging.getLogger(__name__)\n\n\nclass WalletManager:\n\n    def __init__(self, wallets: MutableSequence[Wallet] = None,\n                 ledgers: MutableMapping[Type[Ledger], Ledger] = None) -> None:\n        self.wallets = wallets or []\n        self.ledgers = ledgers or {}\n        self.running = False\n        self.config: Optional[Config] = None\n\n    @classmethod\n    def from_config(cls, config: dict) -> 'WalletManager':\n        manager = cls()\n        for ledger_id, ledger_config in config.get('ledgers', {}).items():\n            manager.get_or_create_ledger(ledger_id, ledger_config)\n        for wallet_path in config.get('wallets', []):\n            wallet_storage = WalletStorage(wallet_path)\n            wallet = Wallet.from_storage(wallet_storage, manager)\n            manager.wallets.append(wallet)\n        return manager\n\n    def get_or_create_ledger(self, ledger_id, ledger_config=None):\n        ledger_class = LedgerRegistry.get_ledger_class(ledger_id)\n        ledger = self.ledgers.get(ledger_class)\n        if ledger is None:\n            ledger = ledger_class(ledger_config or {})\n            self.ledgers[ledger_class] = ledger\n        return ledger\n\n    def import_wallet(self, path):\n        storage = WalletStorage(path)\n        wallet = Wallet.from_storage(storage, self)\n        self.wallets.append(wallet)\n        return wallet\n\n    @property\n    def default_wallet(self):\n        for wallet in self.wallets:\n            return wallet\n\n    @property\n    def default_account(self):\n        for wallet in self.wallets:\n            return wallet.default_account\n\n    @property\n    def accounts(self):\n        for wallet in self.wallets:\n            yield from wallet.accounts\n\n    async def start(self):\n        self.running = True\n        await asyncio.gather(*(\n            l.start() for l in self.ledgers.values()\n        ))\n\n    async def stop(self):\n        await asyncio.gather(*(\n            l.stop() for l in self.ledgers.values()\n        ))\n        self.running = False\n\n    def get_wallet_or_default(self, wallet_id: Optional[str]) -> Wallet:\n        if wallet_id is None:\n            return self.default_wallet\n        return self.get_wallet_or_error(wallet_id)\n\n    def get_wallet_or_error(self, wallet_id: str) -> Wallet:\n        for wallet in self.wallets:\n            if wallet.id == wallet_id:\n                return wallet\n        raise WalletNotLoadedError(wallet_id)\n\n    @staticmethod\n    def get_balance(wallet):\n        accounts = wallet.accounts\n        if not accounts:\n            return 0\n        return accounts[0].ledger.db.get_balance(wallet=wallet, accounts=accounts)\n\n    @property\n    def ledger(self) -> Ledger:\n        return self.default_account.ledger\n\n    @property\n    def db(self) -> Database:\n        return self.ledger.db\n\n    def check_locked(self):\n        return self.default_wallet.is_locked\n\n    @staticmethod\n    def migrate_lbryum_to_torba(path):\n        if not os.path.exists(path):\n            return None, None\n        with open(path, 'r') as f:\n            unmigrated_json = f.read()\n            unmigrated = json.loads(unmigrated_json)\n        # TODO: After several public releases of new torba based wallet, we can delete\n        #       this lbryum->torba conversion code and require that users who still\n        #       have old structured wallets install one of the earlier releases that\n        #       still has the below conversion code.\n        if 'master_public_keys' not in unmigrated:\n            return None, None\n        total = unmigrated.get('addr_history')\n        receiving_addresses, change_addresses = set(), set()\n        for _, unmigrated_account in unmigrated.get('accounts', {}).items():\n            receiving_addresses.update(map(unhexlify, unmigrated_account.get('receiving', [])))\n            change_addresses.update(map(unhexlify, unmigrated_account.get('change', [])))\n        log.info(\"Wallet migrator found %s receiving addresses and %s change addresses. %s in total on history.\",\n                 len(receiving_addresses), len(change_addresses), len(total))\n\n        migrated_json = json.dumps({\n            'version': 1,\n            'name': 'My Wallet',\n            'accounts': [{\n                'version': 1,\n                'name': 'Main Account',\n                'ledger': 'lbc_mainnet',\n                'encrypted': unmigrated['use_encryption'],\n                'seed': unmigrated['seed'],\n                'seed_version': unmigrated['seed_version'],\n                'private_key': unmigrated['master_private_keys']['x/'],\n                'public_key': unmigrated['master_public_keys']['x/'],\n                'certificates': unmigrated.get('claim_certificates', {}),\n                'address_generator': {\n                    'name': 'deterministic-chain',\n                    'receiving': {'gap': 20, 'maximum_uses_per_address': 1},\n                    'change': {'gap': 6, 'maximum_uses_per_address': 1}\n                }\n            }]\n        }, indent=4, sort_keys=True)\n        mode = os.stat(path).st_mode\n        i = 1\n        backup_path_template = os.path.join(os.path.dirname(path), \"old_lbryum_wallet\") + \"_%i\"\n        while os.path.isfile(backup_path_template % i):\n            i += 1\n        os.rename(path, backup_path_template % i)\n        temp_path = f\"{path}.tmp.{os.getpid()}\"\n        with open(temp_path, \"w\") as f:\n            f.write(migrated_json)\n            f.flush()\n            os.fsync(f.fileno())\n        os.rename(temp_path, path)\n        os.chmod(path, mode)\n        return receiving_addresses, change_addresses\n\n    @classmethod\n    async def from_lbrynet_config(cls, config: Config):\n\n        ledger_id = {\n            'lbrycrd_main':    'lbc_mainnet',\n            'lbrycrd_testnet': 'lbc_testnet',\n            'lbrycrd_regtest': 'lbc_regtest'\n        }[config.blockchain_name]\n\n        ledger_config = {\n            'auto_connect': True,\n            'explicit_servers': [],\n            'hub_timeout': config.hub_timeout,\n            'default_servers': config.lbryum_servers,\n            'known_hubs': config.known_hubs,\n            'jurisdiction': config.jurisdiction,\n            'concurrent_hub_requests': config.concurrent_hub_requests,\n            'data_path': config.wallet_dir,\n            'tx_cache_size': config.transaction_cache_size\n        }\n        if 'LBRY_FEE_PER_NAME_CHAR' in os.environ:\n            ledger_config['fee_per_name_char'] = int(os.environ.get('LBRY_FEE_PER_NAME_CHAR'))\n\n        wallets_directory = os.path.join(config.wallet_dir, 'wallets')\n        if not os.path.exists(wallets_directory):\n            os.mkdir(wallets_directory)\n\n        receiving_addresses, change_addresses = cls.migrate_lbryum_to_torba(\n            os.path.join(wallets_directory, 'default_wallet')\n        )\n\n        if Config.lbryum_servers.is_set_to_default(config):\n            with config.update_config() as c:\n                c.lbryum_servers = NOT_SET\n\n        manager = cls.from_config({\n            'ledgers': {ledger_id: ledger_config},\n            'wallets': [\n                os.path.join(wallets_directory, wallet_file) for wallet_file in config.wallets\n            ]\n        })\n        manager.config = config\n        ledger = manager.get_or_create_ledger(ledger_id)\n        ledger.coin_selection_strategy = config.coin_selection_strategy\n        default_wallet = manager.default_wallet\n        if default_wallet.default_account is None:\n            log.info('Wallet at %s is empty, generating a default account.', default_wallet.id)\n            default_wallet.generate_account(ledger)\n            default_wallet.save()\n        if default_wallet.is_locked and default_wallet.preferences.get(ENCRYPT_ON_DISK) is None:\n            default_wallet.preferences[ENCRYPT_ON_DISK] = True\n            default_wallet.save()\n        if receiving_addresses or change_addresses:\n            if not os.path.exists(ledger.path):\n                os.mkdir(ledger.path)\n            await ledger.db.open()\n            try:\n                await manager._migrate_addresses(receiving_addresses, change_addresses)\n            finally:\n                await ledger.db.close()\n        return manager\n\n    async def reset(self):\n        self.ledger.config = {\n            'auto_connect': True,\n            'explicit_servers': [],\n            'default_servers': Config.lbryum_servers.default,\n            'known_hubs': self.config.known_hubs,\n            'jurisdiction': self.config.jurisdiction,\n            'hub_timeout': self.config.hub_timeout,\n            'concurrent_hub_requests': self.config.concurrent_hub_requests,\n            'data_path': self.config.wallet_dir,\n        }\n        if Config.lbryum_servers.is_set(self.config):\n            self.ledger.config['explicit_servers'] = self.config.lbryum_servers\n        await self.ledger.stop()\n        await self.ledger.start()\n\n    async def _migrate_addresses(self, receiving_addresses: set, change_addresses: set):\n        async with self.default_account.receiving.address_generator_lock:\n            migrated_receiving = set(await self.default_account.receiving._generate_keys(0, len(receiving_addresses)))\n        async with self.default_account.change.address_generator_lock:\n            migrated_change = set(await self.default_account.change._generate_keys(0, len(change_addresses)))\n        receiving_addresses = set(map(self.default_account.ledger.public_key_to_address, receiving_addresses))\n        change_addresses = set(map(self.default_account.ledger.public_key_to_address, change_addresses))\n        if not any(change_addresses.difference(migrated_change)):\n            log.info(\"Successfully migrated %s change addresses.\", len(change_addresses))\n        else:\n            log.warning(\"Failed to migrate %s change addresses!\",\n                        len(set(change_addresses).difference(set(migrated_change))))\n        if not any(receiving_addresses.difference(migrated_receiving)):\n            log.info(\"Successfully migrated %s receiving addresses.\", len(receiving_addresses))\n        else:\n            log.warning(\"Failed to migrate %s receiving addresses!\",\n                        len(set(receiving_addresses).difference(set(migrated_receiving))))\n\n    async def get_best_blockhash(self):\n        if len(self.ledger.headers) <= 0:\n            return self.ledger.genesis_hash\n        return (await self.ledger.headers.hash(self.ledger.headers.height)).decode()\n\n    def get_unused_address(self):\n        return self.default_account.receiving.get_or_create_usable_address()\n\n    async def get_transaction(self, txid: str):\n        tx = await self.db.get_transaction(txid=txid)\n        if tx:\n            return tx\n        try:\n            raw, merkle = await self.ledger.network.get_transaction_and_merkle(txid)\n        except CodeMessageError as e:\n            if 'No such mempool or blockchain transaction.' in e.message:\n                return {'success': False, 'code': 404, 'message': 'transaction not found'}\n            return {'success': False, 'code': e.code, 'message': e.message}\n        height = merkle.get('block_height')\n        tx = Transaction(unhexlify(raw), height=height)\n        if height and height > 0:\n            await self.ledger.maybe_verify_transaction(tx, height, merkle)\n        return tx\n\n    async def create_purchase_transaction(\n            self, accounts: List[Account], txo: Output, exchange: 'ExchangeRateManager',\n            override_max_key_fee=False):\n        fee = txo.claim.stream.fee\n        fee_amount = exchange.to_dewies(fee.currency, fee.amount)\n        if not override_max_key_fee and self.config.max_key_fee:\n            max_fee = self.config.max_key_fee\n            max_fee_amount = exchange.to_dewies(max_fee['currency'], Decimal(max_fee['amount']))\n            if max_fee_amount and fee_amount > max_fee_amount:\n                error_fee = f\"{dewies_to_lbc(fee_amount)} LBC\"\n                if fee.currency != 'LBC':\n                    error_fee += f\" ({fee.amount} {fee.currency})\"\n                error_max_fee = f\"{dewies_to_lbc(max_fee_amount)} LBC\"\n                if max_fee['currency'] != 'LBC':\n                    error_max_fee += f\" ({max_fee['amount']} {max_fee['currency']})\"\n                raise KeyFeeAboveMaxAllowedError(\n                    f\"Purchase price of {error_fee} exceeds maximum \"\n                    f\"configured price of {error_max_fee}.\"\n                )\n        fee_address = fee.address or txo.get_address(self.ledger)\n        return await Transaction.purchase(\n            txo.claim_id, fee_amount, fee_address, accounts, accounts[0]\n        )\n\n    async def broadcast_or_release(self, tx, blocking=False):\n        await self.ledger.broadcast_or_release(tx, blocking=blocking)\n"
  },
  {
    "path": "lbry/wallet/mnemonic.py",
    "content": "# Copyright (C) 2014 Thomas Voegtlin\n# Copyright (C) 2018 LBRY Inc.\n\nimport hmac\nimport math\nimport hashlib\nimport importlib\nimport unicodedata\nimport string\nfrom binascii import hexlify\nfrom secrets import randbelow\n\nimport pbkdf2\n\nfrom lbry.crypto.hash import hmac_sha512\nfrom .words import english\n\n# The hash of the mnemonic seed must begin with this\nSEED_PREFIX = b'01'       # Standard wallet\nSEED_PREFIX_2FA = b'101'  # Two-factor authentication\nSEED_PREFIX_SW = b'100'   # Segwit wallet\n\n# http://www.asahi-net.or.jp/~ax2s-kmtn/ref/unicode/e_asia.html\nCJK_INTERVALS = [\n    (0x4E00, 0x9FFF, 'CJK Unified Ideographs'),\n    (0x3400, 0x4DBF, 'CJK Unified Ideographs Extension A'),\n    (0x20000, 0x2A6DF, 'CJK Unified Ideographs Extension B'),\n    (0x2A700, 0x2B73F, 'CJK Unified Ideographs Extension C'),\n    (0x2B740, 0x2B81F, 'CJK Unified Ideographs Extension D'),\n    (0xF900, 0xFAFF, 'CJK Compatibility Ideographs'),\n    (0x2F800, 0x2FA1D, 'CJK Compatibility Ideographs Supplement'),\n    (0x3190, 0x319F, 'Kanbun'),\n    (0x2E80, 0x2EFF, 'CJK Radicals Supplement'),\n    (0x2F00, 0x2FDF, 'CJK Radicals'),\n    (0x31C0, 0x31EF, 'CJK Strokes'),\n    (0x2FF0, 0x2FFF, 'Ideographic Description Characters'),\n    (0xE0100, 0xE01EF, 'Variation Selectors Supplement'),\n    (0x3100, 0x312F, 'Bopomofo'),\n    (0x31A0, 0x31BF, 'Bopomofo Extended'),\n    (0xFF00, 0xFFEF, 'Halfwidth and Fullwidth Forms'),\n    (0x3040, 0x309F, 'Hiragana'),\n    (0x30A0, 0x30FF, 'Katakana'),\n    (0x31F0, 0x31FF, 'Katakana Phonetic Extensions'),\n    (0x1B000, 0x1B0FF, 'Kana Supplement'),\n    (0xAC00, 0xD7AF, 'Hangul Syllables'),\n    (0x1100, 0x11FF, 'Hangul Jamo'),\n    (0xA960, 0xA97F, 'Hangul Jamo Extended A'),\n    (0xD7B0, 0xD7FF, 'Hangul Jamo Extended B'),\n    (0x3130, 0x318F, 'Hangul Compatibility Jamo'),\n    (0xA4D0, 0xA4FF, 'Lisu'),\n    (0x16F00, 0x16F9F, 'Miao'),\n    (0xA000, 0xA48F, 'Yi Syllables'),\n    (0xA490, 0xA4CF, 'Yi Radicals'),\n]\n\n\ndef is_cjk(c):\n    n = ord(c)\n    for start, end, _ in CJK_INTERVALS:\n        if start <= n <= end:\n            return True\n    return False\n\n\ndef normalize_text(seed):\n    seed = unicodedata.normalize('NFKD', seed)\n    seed = seed.lower()\n    # remove accents\n    seed = ''.join([c for c in seed if not unicodedata.combining(c)])\n    # normalize whitespaces\n    seed = ' '.join(seed.split())\n    # remove whitespaces between CJK\n    seed = ''.join([\n        seed[i] for i in range(len(seed))\n        if not (seed[i] in string.whitespace and is_cjk(seed[i-1]) and is_cjk(seed[i+1]))\n    ])\n    return seed\n\n\ndef load_words(language_name):\n    if language_name == 'english':\n        return english.words\n    language_module = importlib.import_module('lbry.wallet.client.words.'+language_name)\n    return list(map(\n        lambda s: unicodedata.normalize('NFKD', s),\n        language_module.words\n    ))\n\n\nLANGUAGE_NAMES = {\n    'en': 'english',\n    'es': 'spanish',\n    'ja': 'japanese',\n    'pt': 'portuguese',\n    'zh': 'chinese_simplified'\n}\n\n\nclass Mnemonic:\n    # Seed derivation no longer follows BIP39\n    # Mnemonic phrase uses a hash based checksum, instead of a words-dependent checksum\n\n    def __init__(self, lang='en'):\n        language_name = LANGUAGE_NAMES.get(lang, 'english')\n        self.words = load_words(language_name)\n\n    @staticmethod\n    def mnemonic_to_seed(mnemonic, passphrase=''):\n        pbkdf2_rounds = 2048\n        mnemonic = normalize_text(mnemonic)\n        passphrase = normalize_text(passphrase)\n        return pbkdf2.PBKDF2(\n            mnemonic, passphrase, iterations=pbkdf2_rounds, macmodule=hmac, digestmodule=hashlib.sha512\n        ).read(64)\n\n    def mnemonic_encode(self, i):\n        n = len(self.words)\n        words = []\n        while i:\n            x = i%n\n            i = i//n\n            words.append(self.words[x])\n        return ' '.join(words)\n\n    def mnemonic_decode(self, seed):\n        n = len(self.words)\n        words = seed.split()\n        i = 0\n        while words:\n            word = words.pop()\n            k = self.words.index(word)\n            i = i*n + k\n        return i\n\n    def make_seed(self, prefix=SEED_PREFIX, num_bits=132):\n        # increase num_bits in order to obtain a uniform distribution for the last word\n        bpw = math.log(len(self.words), 2)\n        # rounding\n        n = int(math.ceil(num_bits/bpw) * bpw)\n        entropy = 1\n        while 0 < entropy < pow(2, n - bpw):\n            # try again if seed would not contain enough words\n            entropy = randbelow(pow(2, n))\n        nonce = 0\n        while True:\n            nonce += 1\n            i = entropy + nonce\n            seed = self.mnemonic_encode(i)\n            if i != self.mnemonic_decode(seed):\n                raise Exception('Cannot extract same entropy from mnemonic!')\n            if is_new_seed(seed, prefix):\n                break\n        return seed\n\n\ndef is_new_seed(seed, prefix):\n    seed = normalize_text(seed)\n    seed_hash = hexlify(hmac_sha512(b\"Seed version\", seed.encode('utf8')))\n    return seed_hash.startswith(prefix)\n"
  },
  {
    "path": "lbry/wallet/network.py",
    "content": "import logging\nimport asyncio\nimport json\nimport socket\nimport random\nfrom time import perf_counter\nfrom collections import defaultdict\nfrom typing import Dict, Optional, Tuple\nimport aiohttp\n\nfrom lbry import __version__\nfrom lbry.utils import resolve_host\nfrom lbry.error import IncompatibleWalletServerError\nfrom lbry.wallet.rpc import RPCSession as BaseClientSession, Connector, RPCError, ProtocolError\nfrom lbry.wallet.stream import StreamController\nfrom lbry.wallet.udp import SPVStatusClientProtocol, SPVPong\nfrom lbry.conf import KnownHubsList\n\nlog = logging.getLogger(__name__)\n\n\nclass ClientSession(BaseClientSession):\n    def __init__(self, *args, network: 'Network', server, timeout=30, concurrency=32, **kwargs):\n        self.network = network\n        self.server = server\n        super().__init__(*args, **kwargs)\n        self.framer.max_size = self.max_errors = 1 << 32\n        self.timeout = timeout\n        self.max_seconds_idle = timeout * 2\n        self.response_time: Optional[float] = None\n        self.connection_latency: Optional[float] = None\n        self._response_samples = 0\n        self._concurrency = asyncio.Semaphore(concurrency)\n\n    @property\n    def concurrency(self):\n        return self._concurrency._value\n\n    @property\n    def available(self):\n        return not self.is_closing() and self.response_time is not None\n\n    @property\n    def server_address_and_port(self) -> Optional[Tuple[str, int]]:\n        if not self.transport:\n            return None\n        return self.transport.get_extra_info('peername')\n\n    async def send_timed_server_version_request(self, args=(), timeout=None):\n        timeout = timeout or self.timeout\n        log.debug(\"send version request to %s:%i\", *self.server)\n        start = perf_counter()\n        result = await asyncio.wait_for(\n            super().send_request('server.version', args), timeout=timeout\n        )\n        current_response_time = perf_counter() - start\n        response_sum = (self.response_time or 0) * self._response_samples + current_response_time\n        self.response_time = response_sum / (self._response_samples + 1)\n        self._response_samples += 1\n        return result\n\n    async def send_request(self, method, args=()):\n        log.debug(\"send %s%s to %s:%i (%i timeout)\", method, tuple(args), self.server[0], self.server[1], self.timeout)\n        try:\n            await self._concurrency.acquire()\n            if method == 'server.version':\n                return await self.send_timed_server_version_request(args, self.timeout)\n            request = asyncio.ensure_future(super().send_request(method, args))\n            while not request.done():\n                done, pending = await asyncio.wait([request], timeout=self.timeout)\n                if pending:\n                    log.debug(\"Time since last packet: %s\", perf_counter() - self.last_packet_received)\n                    if (perf_counter() - self.last_packet_received) < self.timeout:\n                        continue\n                    log.warning(\"timeout sending %s to %s:%i\", method, *self.server)\n                    raise asyncio.TimeoutError\n                if done:\n                    try:\n                        return request.result()\n                    except ConnectionResetError:\n                        log.error(\n                            \"wallet server (%s) reset connection upon our %s request, json of %i args is %i bytes\",\n                            self.server[0], method, len(args), len(json.dumps(args))\n                        )\n                        raise\n        except (RPCError, ProtocolError) as e:\n            log.warning(\"Wallet server (%s:%i) returned an error. Code: %s Message: %s\",\n                        *self.server, *e.args)\n            raise e\n        except ConnectionError:\n            log.warning(\"connection to %s:%i lost\", *self.server)\n            self.synchronous_close()\n            raise\n        except asyncio.CancelledError:\n            log.warning(\"cancelled sending %s to %s:%i\", method, *self.server)\n            # self.synchronous_close()\n            raise\n        finally:\n            self._concurrency.release()\n\n    async def ensure_server_version(self, required=None, timeout=3):\n        required = required or self.network.PROTOCOL_MAX_VERSION\n        response = await asyncio.wait_for(\n            self.send_request('server.version', [self.network.CLIENT_NAME, required]), timeout=timeout\n        )\n        if tuple(int(piece) for piece in response[1].split(\".\")) >= self.network.PROTOCOL_MIN_VERSION:\n            return response\n        raise IncompatibleWalletServerError(*self.server)\n\n    async def keepalive_loop(self, timeout=3, max_idle=60):\n        try:\n            while True:\n                now = perf_counter()\n                if min(self.last_send, self.last_packet_received) + max_idle < now:\n                    await asyncio.wait_for(\n                        self.send_request('server.ping', []), timeout=timeout\n                    )\n                else:\n                    await asyncio.sleep(max(0, max_idle - (now - self.last_send)))\n        except (Exception, asyncio.CancelledError) as err:\n            if isinstance(err, asyncio.CancelledError):\n                log.info(\"closing connection to %s:%i\", *self.server)\n            else:\n                log.exception(\"lost connection to spv\")\n        finally:\n            if not self.is_closing():\n                self._close()\n\n    async def create_connection(self, timeout=6):\n        connector = Connector(lambda: self, *self.server)\n        start = perf_counter()\n        await asyncio.wait_for(connector.create_connection(), timeout=timeout)\n        self.connection_latency = perf_counter() - start\n\n    async def handle_request(self, request):\n        controller = self.network.subscription_controllers[request.method]\n        controller.add(request.args)\n\n    def connection_lost(self, exc):\n        log.debug(\"Connection lost: %s:%d\", *self.server)\n        super().connection_lost(exc)\n        self.response_time = None\n        self.connection_latency = None\n        self._response_samples = 0\n        # self._on_disconnect_controller.add(True)\n        if self.network:\n            self.network.disconnect()\n\n\nclass Network:\n\n    CLIENT_VERSION = __version__\n    CLIENT_NAME = \"LBRY SDK \" + CLIENT_VERSION\n\n    PROTOCOL_MIN_VERSION = (0, 65, 0)\n    PROTOCOL_MAX_VERSION = __version__\n\n    def __init__(self, ledger):\n        self.ledger = ledger\n        self.client: Optional[ClientSession] = None\n        self.server_features = None\n        # self._switch_task: Optional[asyncio.Task] = None\n        self.running = False\n        self.remote_height: int = 0\n\n        self._on_connected_controller = StreamController()\n        self.on_connected = self._on_connected_controller.stream\n\n        self._on_header_controller = StreamController(merge_repeated_events=True)\n        self.on_header = self._on_header_controller.stream\n\n        self._on_status_controller = StreamController(merge_repeated_events=True)\n        self.on_status = self._on_status_controller.stream\n\n        self._on_hub_controller = StreamController(merge_repeated_events=True)\n        self.on_hub = self._on_hub_controller.stream\n\n        self.subscription_controllers = {\n            'blockchain.headers.subscribe': self._on_header_controller,\n            'blockchain.address.subscribe': self._on_status_controller,\n            'blockchain.peers.subscribe': self._on_hub_controller,\n        }\n\n        self.aiohttp_session: Optional[aiohttp.ClientSession] = None\n        self._urgent_need_reconnect = asyncio.Event()\n        self._loop_task: Optional[asyncio.Task] = None\n        self._keepalive_task: Optional[asyncio.Task] = None\n\n    @property\n    def config(self):\n        return self.ledger.config\n\n    @property\n    def known_hubs(self):\n        if 'known_hubs' not in self.config:\n            return KnownHubsList()\n        return self.config['known_hubs']\n\n    @property\n    def jurisdiction(self):\n        return self.config.get(\"jurisdiction\")\n\n    def disconnect(self):\n        if self._keepalive_task and not self._keepalive_task.done():\n            self._keepalive_task.cancel()\n        self._keepalive_task = None\n\n    async def start(self):\n        if not self.running:\n            self.running = True\n            self.aiohttp_session = aiohttp.ClientSession()\n            self.on_header.listen(self._update_remote_height)\n            self.on_hub.listen(self._update_hubs)\n            self._loop_task = asyncio.create_task(self.network_loop())\n            self._urgent_need_reconnect.set()\n\n        def loop_task_done_callback(f):\n            try:\n                f.result()\n            except (Exception, asyncio.CancelledError):\n                if self.running:\n                    log.exception(\"wallet server connection loop crashed\")\n\n        self._loop_task.add_done_callback(loop_task_done_callback)\n\n    async def resolve_spv_dns(self):\n        hostname_to_ip = {}\n        ip_to_hostnames = defaultdict(list)\n\n        async def resolve_spv(server, port):\n            try:\n                server_addr = await resolve_host(server, port, 'udp')\n                hostname_to_ip[server] = (server_addr, port)\n                ip_to_hostnames[(server_addr, port)].append(server)\n            except socket.error:\n                log.warning(\"error looking up dns for spv server %s:%i\", server, port)\n            except Exception:\n                log.exception(\"error looking up dns for spv server %s:%i\", server, port)\n\n        # accumulate the dns results\n        if self.config.get('explicit_servers', []):\n            hubs = self.config['explicit_servers']\n        elif self.known_hubs:\n            hubs = self.known_hubs\n        else:\n            hubs = self.config['default_servers']\n        await asyncio.gather(*(resolve_spv(server, port) for (server, port) in hubs))\n        return hostname_to_ip, ip_to_hostnames\n\n    async def get_n_fastest_spvs(self, timeout=3.0) -> Dict[Tuple[str, int], Optional[SPVPong]]:\n        loop = asyncio.get_event_loop()\n        pong_responses = asyncio.Queue()\n        connection = SPVStatusClientProtocol(pong_responses)\n        sent_ping_timestamps = {}\n        _, ip_to_hostnames = await self.resolve_spv_dns()\n        n = len(ip_to_hostnames)\n        log.info(\"%i possible spv servers to try (%i urls in config)\", n, len(self.config.get('explicit_servers', [])))\n        pongs = {}\n        known_hubs = self.known_hubs\n        try:\n            await loop.create_datagram_endpoint(lambda: connection, ('0.0.0.0', 0))\n            # could raise OSError if it cant bind\n            start = perf_counter()\n            for server in ip_to_hostnames:\n                connection.ping(server)\n                sent_ping_timestamps[server] = perf_counter()\n            while len(pongs) < n:\n                (remote, ts), pong = await asyncio.wait_for(pong_responses.get(), timeout - (perf_counter() - start))\n                latency = ts - start\n                log.info(\"%s:%i has latency of %sms (available: %s, height: %i)\",\n                         '/'.join(ip_to_hostnames[remote]), remote[1], round(latency * 1000, 2),\n                         pong.available, pong.height)\n\n                known_hubs.hubs.setdefault((ip_to_hostnames[remote][0], remote[1]), {}).update(\n                    {\"country\": pong.country_name}\n                )\n                if pong.available:\n                    pongs[(ip_to_hostnames[remote][0], remote[1])] = pong\n            return pongs\n        except asyncio.TimeoutError:\n            if pongs:\n                log.info(\"%i/%i probed spv servers are accepting connections\", len(pongs), len(ip_to_hostnames))\n                return pongs\n            else:\n                log.warning(\"%i spv status probes failed, retrying later. servers tried: %s\",\n                            len(sent_ping_timestamps),\n                            ', '.join('/'.join(hosts) + f' ({ip})' for ip, hosts in ip_to_hostnames.items()))\n                random_server = random.choice(list(ip_to_hostnames.keys()))\n                host, port = random_server\n                log.warning(\"trying fallback to randomly selected spv: %s:%i\", host, port)\n                known_hubs.hubs.setdefault((host, port), {})\n                return {(host, port): None}\n        finally:\n            connection.close()\n\n    async def connect_to_fastest(self) -> Optional[ClientSession]:\n        fastest_spvs = await self.get_n_fastest_spvs()\n        for (host, port), pong in fastest_spvs.items():\n            if (pong is not None and self.jurisdiction is not None) and \\\n                    (pong.country_name != self.jurisdiction):\n                continue\n            client = ClientSession(network=self, server=(host, port), timeout=self.config.get('hub_timeout', 30),\n                                   concurrency=self.config.get('concurrent_hub_requests', 30))\n            try:\n                await client.create_connection()\n                log.info(\"Connected to spv server %s:%i\", host, port)\n                await client.ensure_server_version()\n                return client\n            except (asyncio.TimeoutError, ConnectionError, OSError, IncompatibleWalletServerError, RPCError):\n                log.warning(\"Connecting to %s:%d failed\", host, port)\n                client._close()\n        return\n\n    async def network_loop(self):\n        sleep_delay = 30\n        while self.running:\n            await asyncio.wait(\n                map(asyncio.create_task, [asyncio.sleep(30), self._urgent_need_reconnect.wait()]),\n                return_when=asyncio.FIRST_COMPLETED\n            )\n            if self._urgent_need_reconnect.is_set():\n                sleep_delay = 30\n            self._urgent_need_reconnect.clear()\n            if not self.is_connected:\n                client = await self.connect_to_fastest()\n                if not client:\n                    log.warning(\"failed to connect to any spv servers, retrying later\")\n                    sleep_delay *= 2\n                    sleep_delay = min(sleep_delay, 300)\n                    continue\n                log.debug(\"get spv server features %s:%i\", *client.server)\n                features = await client.send_request('server.features', [])\n                self.client, self.server_features = client, features\n                log.debug(\"discover other hubs %s:%i\", *client.server)\n                await self._update_hubs(await client.send_request('server.peers.subscribe', []))\n                log.info(\"subscribe to headers %s:%i\", *client.server)\n                self._update_remote_height((await self.subscribe_headers(),))\n                self._on_connected_controller.add(True)\n                server_str = \"%s:%i\" % client.server\n                log.info(\"maintaining connection to spv server %s\", server_str)\n                self._keepalive_task = asyncio.create_task(self.client.keepalive_loop())\n                try:\n                    if not self._urgent_need_reconnect.is_set():\n                        await asyncio.wait(\n                            [self._keepalive_task, asyncio.create_task(self._urgent_need_reconnect.wait())],\n                            return_when=asyncio.FIRST_COMPLETED\n                        )\n                    else:\n                        await self._keepalive_task\n                    if self._urgent_need_reconnect.is_set():\n                        log.warning(\"urgent reconnect needed\")\n                    if self._keepalive_task and not self._keepalive_task.done():\n                        self._keepalive_task.cancel()\n                except asyncio.CancelledError:\n                    pass\n                finally:\n                    self._keepalive_task = None\n                    self.client = None\n                    self.server_features = None\n                    log.info(\"connection lost to %s\", server_str)\n        log.info(\"network loop finished\")\n\n    async def stop(self):\n        self.running = False\n        self.disconnect()\n        if self._loop_task and not self._loop_task.done():\n            self._loop_task.cancel()\n        self._loop_task = None\n        if self.aiohttp_session:\n            await self.aiohttp_session.close()\n            self.aiohttp_session = None\n\n    @property\n    def is_connected(self):\n        return self.client and not self.client.is_closing()\n\n    def rpc(self, list_or_method, args, restricted=True, session: Optional[ClientSession] = None):\n        if session or self.is_connected:\n            session = session or self.client\n            return session.send_request(list_or_method, args)\n        else:\n            self._urgent_need_reconnect.set()\n            raise ConnectionError(\"Attempting to send rpc request when connection is not available.\")\n\n    async def retriable_call(self, function, *args, **kwargs):\n        while self.running:\n            if not self.is_connected:\n                log.warning(\"Wallet server unavailable, waiting for it to come back and retry.\")\n                self._urgent_need_reconnect.set()\n                await self.on_connected.first\n            try:\n                return await function(*args, **kwargs)\n            except asyncio.TimeoutError:\n                log.warning(\"Wallet server call timed out, retrying.\")\n            except ConnectionError:\n                log.warning(\"connection error\")\n        raise asyncio.CancelledError()  # if we got here, we are shutting down\n\n    def _update_remote_height(self, header_args):\n        self.remote_height = header_args[0][\"height\"]\n\n    async def _update_hubs(self, hubs):\n        if hubs and hubs != ['']:\n            try:\n                if self.known_hubs.add_hubs(hubs):\n                    self.known_hubs.save()\n            except Exception:\n                log.exception(\"could not add hubs: %s\", hubs)\n\n    def get_transaction(self, tx_hash, known_height=None):\n        # use any server if its old, otherwise restrict to who gave us the history\n        restricted = known_height in (None, -1, 0) or 0 > known_height > self.remote_height - 10\n        return self.rpc('blockchain.transaction.get', [tx_hash], restricted)\n\n    def get_transaction_batch(self, txids, restricted=True):\n        # use any server if its old, otherwise restrict to who gave us the history\n        return self.rpc('blockchain.transaction.get_batch', txids, restricted)\n\n    def get_transaction_and_merkle(self, tx_hash, known_height=None):\n        # use any server if its old, otherwise restrict to who gave us the history\n        restricted = known_height in (None, -1, 0) or 0 > known_height > self.remote_height - 10\n        return self.rpc('blockchain.transaction.info', [tx_hash], restricted)\n\n    def get_transaction_height(self, tx_hash, known_height=None):\n        restricted = not known_height or 0 > known_height > self.remote_height - 10\n        return self.rpc('blockchain.transaction.get_height', [tx_hash], restricted)\n\n    def get_merkle(self, tx_hash, height):\n        restricted = 0 > height > self.remote_height - 10\n        return self.rpc('blockchain.transaction.get_merkle', [tx_hash, height], restricted)\n\n    def get_headers(self, height, count=10000, b64=False):\n        restricted = height >= self.remote_height - 100\n        return self.rpc('blockchain.block.headers', [height, count, 0, b64], restricted)\n\n    #  --- Subscribes, history and broadcasts are always aimed towards the master client directly\n    def get_history(self, address):\n        return self.rpc('blockchain.address.get_history', [address], True)\n\n    def broadcast(self, raw_transaction):\n        return self.rpc('blockchain.transaction.broadcast', [raw_transaction], True)\n\n    def subscribe_headers(self):\n        return self.rpc('blockchain.headers.subscribe', [True], True)\n\n    async def subscribe_address(self, address, *addresses):\n        addresses = list((address, ) + addresses)\n        server_addr_and_port = self.client.server_address_and_port  # on disconnect client will be None\n        try:\n            return await self.rpc('blockchain.address.subscribe', addresses, True)\n        except asyncio.TimeoutError:\n            log.warning(\n                \"timed out subscribing to addresses from %s:%i\",\n                *server_addr_and_port\n            )\n            # abort and cancel, we can't lose a subscription, it will happen again on reconnect\n            if self.client:\n                self.client.abort()\n            raise asyncio.CancelledError()\n\n    def unsubscribe_address(self, address):\n        return self.rpc('blockchain.address.unsubscribe', [address], True)\n\n    def get_server_features(self):\n        return self.rpc('server.features', (), restricted=True)\n\n    # def get_claims_by_ids(self, claim_ids):\n    #     return self.rpc('blockchain.claimtrie.getclaimsbyids', claim_ids)\n\n    def get_claim_by_id(self, claim_id):\n        return self.rpc('blockchain.claimtrie.getclaimbyid', [claim_id])\n\n    def resolve(self, urls, session_override=None):\n        return self.rpc('blockchain.claimtrie.resolve', urls, False, session_override)\n\n    def claim_search(self, session_override=None, **kwargs):\n        return self.rpc('blockchain.claimtrie.search', kwargs, False, session_override)\n\n    async def sum_supports(self, server, **kwargs):\n        message = {\"method\": \"support_sum\", \"params\": kwargs}\n        async with self.aiohttp_session.post(server, json=message) as r:\n            result = await r.json()\n            return result['result']\n"
  },
  {
    "path": "lbry/wallet/orchstr8/__init__.py",
    "content": "from lbry.wallet.orchstr8.node import Conductor\nfrom lbry.wallet.orchstr8.service import ConductorService\n"
  },
  {
    "path": "lbry/wallet/orchstr8/cli.py",
    "content": "import logging\nimport argparse\nimport asyncio\nimport aiohttp\n\nfrom lbry import wallet\nfrom lbry.wallet.orchstr8.node import (\n    Conductor,\n    get_lbcd_node_from_ledger,\n    get_lbcwallet_node_from_ledger\n)\nfrom lbry.wallet.orchstr8.service import ConductorService\n\n\ndef get_argument_parser():\n    parser = argparse.ArgumentParser(\n        prog=\"orchstr8\"\n    )\n    subparsers = parser.add_subparsers(dest='command', help='sub-command help')\n\n    subparsers.add_parser(\"download\", help=\"Download lbcd and lbcwallet node binaries.\")\n\n    start = subparsers.add_parser(\"start\", help=\"Start orchstr8 service.\")\n    start.add_argument(\"--lbcd\", help=\"Hostname to start lbcd node.\")\n    start.add_argument(\"--lbcwallet\", help=\"Hostname to start lbcwallet node.\")\n    start.add_argument(\"--spv\", help=\"Hostname to start SPV server.\")\n    start.add_argument(\"--wallet\", help=\"Hostname to start wallet daemon.\")\n\n    generate = subparsers.add_parser(\"generate\", help=\"Call generate method on running orchstr8 instance.\")\n    generate.add_argument(\"blocks\", type=int, help=\"Number of blocks to generate\")\n\n    subparsers.add_parser(\"transfer\", help=\"Call transfer method on running orchstr8 instance.\")\n    return parser\n\n\nasync def run_remote_command(command, **kwargs):\n    async with aiohttp.ClientSession() as session:\n        async with session.post('http://localhost:7954/'+command, data=kwargs) as resp:\n            print(resp.status)\n            print(await resp.text())\n\n\ndef main():\n    parser = get_argument_parser()\n    args = parser.parse_args()\n    command = getattr(args, 'command', 'help')\n\n    loop = asyncio.get_event_loop()\n    asyncio.set_event_loop(loop)\n\n    if command == 'download':\n        logging.getLogger('blockchain').setLevel(logging.INFO)\n        get_lbcd_node_from_ledger(wallet).ensure()\n        get_lbcwallet_node_from_ledger(wallet).ensure()\n\n    elif command == 'generate':\n        loop.run_until_complete(run_remote_command(\n            'generate', blocks=args.blocks\n        ))\n\n    elif command == 'start':\n\n        conductor = Conductor()\n        if getattr(args, 'lbcd', False):\n            conductor.lbcd_node.hostname = args.lbcd\n            loop.run_until_complete(conductor.start_lbcd())\n        if getattr(args, 'lbcwallet', False):\n            conductor.lbcwallet_node.hostname = args.lbcwallet\n            loop.run_until_complete(conductor.start_lbcwallet())\n        if getattr(args, 'spv', False):\n            conductor.spv_node.hostname = args.spv\n            loop.run_until_complete(conductor.start_spv())\n        if getattr(args, 'wallet', False):\n            conductor.wallet_node.hostname = args.wallet\n            loop.run_until_complete(conductor.start_wallet())\n\n        service = ConductorService(conductor, loop)\n        loop.run_until_complete(service.start())\n\n        try:\n            print('========== Orchstr8 API Service Started ========')\n            loop.run_forever()\n        except KeyboardInterrupt:\n            pass\n        finally:\n            loop.run_until_complete(service.stop())\n            loop.run_until_complete(conductor.stop())\n\n        loop.close()\n\n    else:\n        parser.print_help()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "lbry/wallet/orchstr8/node.py",
    "content": "# pylint: disable=import-error\nimport os\nimport json\nimport shutil\nimport asyncio\nimport zipfile\nimport tarfile\nimport logging\nimport tempfile\nimport subprocess\nimport platform\n\nfrom binascii import hexlify\nfrom typing import Type, Optional\nimport urllib.request\nfrom uuid import uuid4\n\n\nimport lbry\nfrom lbry.wallet import Wallet, Ledger, RegTestLedger, WalletManager, Account, BlockHeightEvent\nfrom lbry.conf import KnownHubsList, Config\n\nlog = logging.getLogger(__name__)\n\ntry:\n    from hub.herald.env import ServerEnv\n    from hub.scribe.env import BlockchainEnv\n    from hub.elastic_sync.env import ElasticEnv\n    from hub.herald.service import HubServerService\n    from hub.elastic_sync.service import ElasticSyncService\n    from hub.scribe.service import BlockchainProcessorService\nexcept ImportError:\n    pass\n\n\ndef get_lbcd_node_from_ledger(ledger_module):\n    return LBCDNode(\n        ledger_module.__lbcd_url__,\n        ledger_module.__lbcd__,\n        ledger_module.__lbcctl__\n    )\n\n\ndef get_lbcwallet_node_from_ledger(ledger_module):\n    return LBCWalletNode(\n        ledger_module.__lbcwallet_url__,\n        ledger_module.__lbcwallet__,\n        ledger_module.__lbcctl__\n    )\n\n\nclass Conductor:\n\n    def __init__(self, seed=None):\n        self.manager_module = WalletManager\n        self.lbcd_node = get_lbcd_node_from_ledger(lbry.wallet)\n        self.lbcwallet_node = get_lbcwallet_node_from_ledger(lbry.wallet)\n        self.spv_node = SPVNode()\n        self.wallet_node = WalletNode(\n            self.manager_module, RegTestLedger, default_seed=seed\n        )\n        self.lbcd_started = False\n        self.lbcwallet_started = False\n        self.spv_started = False\n        self.wallet_started = False\n\n        self.log = log.getChild('conductor')\n\n    async def start_lbcd(self):\n        if not self.lbcd_started:\n            await self.lbcd_node.start()\n            self.lbcd_started = True\n\n    async def stop_lbcd(self, cleanup=True):\n        if self.lbcd_started:\n            await self.lbcd_node.stop(cleanup)\n            self.lbcd_started = False\n\n    async def start_spv(self):\n        if not self.spv_started:\n            await self.spv_node.start(self.lbcwallet_node)\n            self.spv_started = True\n\n    async def stop_spv(self, cleanup=True):\n        if self.spv_started:\n            await self.spv_node.stop(cleanup)\n            self.spv_started = False\n\n    async def start_wallet(self):\n        if not self.wallet_started:\n            await self.wallet_node.start(self.spv_node)\n            self.wallet_started = True\n\n    async def stop_wallet(self, cleanup=True):\n        if self.wallet_started:\n            await self.wallet_node.stop(cleanup)\n            self.wallet_started = False\n\n    async def start_lbcwallet(self, clean=True):\n        if not self.lbcwallet_started:\n            await self.lbcwallet_node.start()\n            if clean:\n                mining_addr = await self.lbcwallet_node.get_new_address()\n                self.lbcwallet_node.mining_addr = mining_addr\n                await self.lbcwallet_node.generate(200)\n            # unlock the wallet for the next 1 hour\n            await self.lbcwallet_node.wallet_passphrase(\"password\", 3600)\n            self.lbcwallet_started = True\n\n    async def stop_lbcwallet(self, cleanup=True):\n        if self.lbcwallet_started:\n            await self.lbcwallet_node.stop(cleanup)\n            self.lbcwallet_started = False\n\n    async def start(self):\n        await self.start_lbcd()\n        await self.start_lbcwallet()\n        await self.start_spv()\n        await self.start_wallet()\n\n    async def stop(self):\n        all_the_stops = [\n            self.stop_wallet,\n            self.stop_spv,\n            self.stop_lbcwallet,\n            self.stop_lbcd\n        ]\n        for stop in all_the_stops:\n            try:\n                await stop()\n            except Exception as e:\n                log.exception('Exception raised while stopping services:', exc_info=e)\n\n    async def clear_mempool(self):\n        await self.stop_lbcwallet(cleanup=False)\n        await self.stop_lbcd(cleanup=False)\n        await self.start_lbcd()\n        await self.start_lbcwallet(clean=False)\n\n\nclass WalletNode:\n\n    def __init__(self, manager_class: Type[WalletManager], ledger_class: Type[Ledger],\n                 verbose: bool = False, port: int = 5280, default_seed: str = None,\n                 data_path: str = None) -> None:\n        self.manager_class = manager_class\n        self.ledger_class = ledger_class\n        self.verbose = verbose\n        self.manager: Optional[WalletManager] = None\n        self.ledger: Optional[Ledger] = None\n        self.wallet: Optional[Wallet] = None\n        self.account: Optional[Account] = None\n        self.data_path: str = data_path or tempfile.mkdtemp()\n        self.port = port\n        self.default_seed = default_seed\n        self.known_hubs = KnownHubsList()\n\n    async def start(self, spv_node: 'SPVNode', seed=None, connect=True, config=None):\n        wallets_dir = os.path.join(self.data_path, 'wallets')\n        wallet_file_name = os.path.join(wallets_dir, 'my_wallet.json')\n        if not os.path.isdir(wallets_dir):\n            os.mkdir(wallets_dir)\n            with open(wallet_file_name, 'w') as wallet_file:\n                wallet_file.write('{\"version\": 1, \"accounts\": []}\\n')\n        self.manager = self.manager_class.from_config({\n            'ledgers': {\n                self.ledger_class.get_id(): {\n                    'api_port': self.port,\n                    'explicit_servers': [(spv_node.hostname, spv_node.port)],\n                    'default_servers': Config.lbryum_servers.default,\n                    'data_path': self.data_path,\n                    'known_hubs': config.known_hubs if config else KnownHubsList(),\n                    'hub_timeout': 30,\n                    'concurrent_hub_requests': 32,\n                    'fee_per_name_char': 200000\n                }\n            },\n            'wallets': [wallet_file_name]\n        })\n        self.manager.config = config\n        self.ledger = self.manager.ledgers[self.ledger_class]\n        self.wallet = self.manager.default_wallet\n        if not self.wallet:\n            raise ValueError('Wallet is required.')\n        if seed or self.default_seed:\n            Account.from_dict(\n                self.ledger, self.wallet, {'seed': seed or self.default_seed}\n            )\n        else:\n            self.wallet.generate_account(self.ledger)\n        self.account = self.wallet.default_account\n        if connect:\n            await self.manager.start()\n\n    async def stop(self, cleanup=True):\n        try:\n            await self.manager.stop()\n        finally:\n            cleanup and self.cleanup()\n\n    def cleanup(self):\n        shutil.rmtree(self.data_path, ignore_errors=True)\n\n\nclass SPVNode:\n    def __init__(self, node_number=1):\n        self.node_number = node_number\n        self.controller = None\n        self.data_path = None\n        self.server: Optional[HubServerService] = None\n        self.writer: Optional[BlockchainProcessorService] = None\n        self.es_writer: Optional[ElasticSyncService] = None\n        self.hostname = 'localhost'\n        self.port = 50001 + node_number  # avoid conflict with default daemon\n        self.udp_port = self.port\n        self.elastic_notifier_port = 19080 + node_number\n        self.elastic_services = f'localhost:9200/localhost:{self.elastic_notifier_port}'\n        self.session_timeout = 600\n        self.stopped = True\n        self.index_name = uuid4().hex\n\n    async def start(self, lbcwallet_node: 'LBCWalletNode', extraconf=None):\n        if not self.stopped:\n            log.warning(\"spv node is already running\")\n            return\n        self.stopped = False\n        try:\n            self.data_path = tempfile.mkdtemp()\n            conf = {\n                'description': '',\n                'payment_address': '',\n                'daily_fee': '0',\n                'db_dir': self.data_path,\n                'daemon_url': lbcwallet_node.rpc_url,\n                'reorg_limit': 100,\n                'host': self.hostname,\n                'tcp_port': self.port,\n                'udp_port': self.udp_port,\n                'elastic_services': self.elastic_services,\n                'session_timeout': self.session_timeout,\n                'max_query_workers': 0,\n                'es_index_prefix': self.index_name,\n                'chain': 'regtest',\n                'index_address_status': False\n            }\n            if extraconf:\n                conf.update(extraconf)\n            self.writer = BlockchainProcessorService(\n                BlockchainEnv(db_dir=self.data_path, daemon_url=lbcwallet_node.rpc_url,\n                              reorg_limit=100, max_query_workers=0, chain='regtest', index_address_status=False)\n            )\n            self.server = HubServerService(ServerEnv(**conf))\n            self.es_writer = ElasticSyncService(\n                ElasticEnv(\n                    db_dir=self.data_path, reorg_limit=100, max_query_workers=0, chain='regtest',\n                    elastic_notifier_port=self.elastic_notifier_port,\n                    es_index_prefix=self.index_name,\n                    filtering_channel_ids=(extraconf or {}).get('filtering_channel_ids'),\n                    blocking_channel_ids=(extraconf or {}).get('blocking_channel_ids')\n                )\n            )\n            await self.writer.start()\n            await self.es_writer.start()\n            await self.server.start()\n        except Exception as e:\n            self.stopped = True\n            log.exception(\"failed to start spv node\")\n            raise e\n\n    async def stop(self, cleanup=True):\n        if self.stopped:\n            log.warning(\"spv node is already stopped\")\n            return\n        try:\n            await self.server.stop()\n            await self.es_writer.delete_index()\n            await self.es_writer.stop()\n            await self.writer.stop()\n            self.stopped = True\n        except Exception as e:\n            log.exception(\"failed to stop spv node\")\n            raise e\n        finally:\n            cleanup and self.cleanup()\n\n    def cleanup(self):\n        shutil.rmtree(self.data_path, ignore_errors=True)\n\n\nclass LBCDProcess(asyncio.SubprocessProtocol):\n\n    IGNORE_OUTPUT = [\n        b'keypool keep',\n        b'keypool reserve',\n        b'keypool return',\n        b'Block submitted',\n    ]\n\n    def __init__(self):\n        self.ready = asyncio.Event()\n        self.stopped = asyncio.Event()\n        self.log = log.getChild('lbcd')\n\n    def pipe_data_received(self, fd, data):\n        if self.log and not any(ignore in data for ignore in self.IGNORE_OUTPUT):\n            if b'Error:' in data:\n                self.log.error(data.decode())\n            else:\n                self.log.info(data.decode())\n        if b'Error:' in data:\n            self.ready.set()\n            raise SystemError(data.decode())\n        if b'RPCS: RPC server listening on' in data:\n            self.ready.set()\n\n    def process_exited(self):\n        self.stopped.set()\n        self.ready.set()\n\n\nclass WalletProcess(asyncio.SubprocessProtocol):\n\n    IGNORE_OUTPUT = [\n    ]\n\n    def __init__(self):\n        self.ready = asyncio.Event()\n        self.stopped = asyncio.Event()\n        self.log = log.getChild('lbcwallet')\n        self.transport: Optional[asyncio.transports.SubprocessTransport] = None\n\n    def pipe_data_received(self, fd, data):\n        if self.log and not any(ignore in data for ignore in self.IGNORE_OUTPUT):\n            if b'Error:' in data:\n                self.log.error(data.decode())\n            else:\n                self.log.info(data.decode())\n        if b'Error:' in data:\n            self.ready.set()\n            raise SystemError(data.decode())\n        if b'WLLT: Finished rescan' in data:\n            self.ready.set()\n\n    def process_exited(self):\n        self.stopped.set()\n        self.ready.set()\n\n\nclass LBCDNode:\n    def __init__(self, url, daemon, cli):\n        self.latest_release_url = url\n        self.project_dir = os.path.dirname(os.path.dirname(__file__))\n        self.bin_dir = os.path.join(self.project_dir, 'bin')\n        self.daemon_bin = os.path.join(self.bin_dir, daemon)\n        self.cli_bin = os.path.join(self.bin_dir, cli)\n        self.log = log.getChild('lbcd')\n        self.data_path = tempfile.mkdtemp()\n        self.protocol = None\n        self.transport = None\n        self.hostname = 'localhost'\n        self.peerport = 29246\n        self.rpcport = 29245\n        self.rpcuser = 'rpcuser'\n        self.rpcpassword = 'rpcpassword'\n        self.stopped = True\n        self.running = asyncio.Event()\n\n    @property\n    def rpc_url(self):\n        return f'http://{self.rpcuser}:{self.rpcpassword}@{self.hostname}:{self.rpcport}/'\n\n    @property\n    def exists(self):\n        return (\n            os.path.exists(self.cli_bin) and\n            os.path.exists(self.daemon_bin)\n        )\n\n    def download(self):\n        uname = platform.uname()\n        target_os = str.lower(uname.system)\n        target_arch = str.replace(uname.machine, 'x86_64', 'amd64')\n        target_platform = target_os + '_' + target_arch\n        self.latest_release_url = str.replace(self.latest_release_url, 'TARGET_PLATFORM', target_platform)\n\n        downloaded_file = os.path.join(\n            self.bin_dir,\n            self.latest_release_url[self.latest_release_url.rfind('/')+1:]\n        )\n\n        if not os.path.exists(self.bin_dir):\n            os.mkdir(self.bin_dir)\n\n        if not os.path.exists(downloaded_file):\n            self.log.info('Downloading: %s', self.latest_release_url)\n            with urllib.request.urlopen(self.latest_release_url) as response:\n                with open(downloaded_file, 'wb') as out_file:\n                    shutil.copyfileobj(response, out_file)\n\n        self.log.info('Extracting: %s', downloaded_file)\n\n        if downloaded_file.endswith('.zip'):\n            with zipfile.ZipFile(downloaded_file) as dotzip:\n                dotzip.extractall(self.bin_dir)\n                # zipfile bug https://bugs.python.org/issue15795\n                os.chmod(self.cli_bin, 0o755)\n                os.chmod(self.daemon_bin, 0o755)\n\n        elif downloaded_file.endswith('.tar.gz'):\n            with tarfile.open(downloaded_file) as tar:\n                tar.extractall(self.bin_dir)\n\n        return self.exists\n\n    def ensure(self):\n        return self.exists or self.download()\n\n    async def start(self):\n        if not self.stopped:\n            return\n        self.stopped = False\n        try:\n            assert self.ensure()\n            loop = asyncio.get_event_loop()\n            asyncio.get_child_watcher().attach_loop(loop)\n            command = [\n                self.daemon_bin,\n                '--notls',\n                f'--datadir={self.data_path}',\n                '--regtest', f'--listen=127.0.0.1:{self.peerport}', f'--rpclisten=127.0.0.1:{self.rpcport}',\n                '--txindex', f'--rpcuser={self.rpcuser}', f'--rpcpass={self.rpcpassword}'\n            ]\n            self.log.info(' '.join(command))\n            self.transport, self.protocol = await loop.subprocess_exec(\n                LBCDProcess, *command\n            )\n            await self.protocol.ready.wait()\n            assert not self.protocol.stopped.is_set()\n            self.running.set()\n        except asyncio.CancelledError:\n            self.running.clear()\n            self.stopped = True\n            raise\n        except Exception as e:\n            self.running.clear()\n            self.stopped = True\n            log.exception('failed to start lbcd', exc_info=e)\n            raise\n\n    async def stop(self, cleanup=True):\n        if self.stopped:\n            return\n        try:\n            if self.transport:\n                self.transport.terminate()\n                await self.protocol.stopped.wait()\n                self.transport.close()\n        except Exception as e:\n            log.exception('failed to stop lbcd', exc_info=e)\n            raise\n        finally:\n            self.log.info(\"Done shutting down \" + self.daemon_bin)\n            self.stopped = True\n            if cleanup:\n                self.cleanup()\n            self.running.clear()\n\n    def cleanup(self):\n        assert self.stopped\n        shutil.rmtree(self.data_path, ignore_errors=True)\n\n\nclass LBCWalletNode:\n    P2SH_SEGWIT_ADDRESS = \"p2sh-segwit\"\n    BECH32_ADDRESS = \"bech32\"\n\n    def __init__(self, url, lbcwallet, cli):\n        self.latest_release_url = url\n        self.project_dir = os.path.dirname(os.path.dirname(__file__))\n        self.bin_dir = os.path.join(self.project_dir, 'bin')\n        self.lbcwallet_bin = os.path.join(self.bin_dir, lbcwallet)\n        self.cli_bin = os.path.join(self.bin_dir, cli)\n        self.log = log.getChild('lbcwallet')\n        self.protocol = None\n        self.transport = None\n        self.hostname = 'localhost'\n        self.lbcd_rpcport = 29245\n        self.lbcwallet_rpcport = 29244\n        self.rpcuser = 'rpcuser'\n        self.rpcpassword = 'rpcpassword'\n        self.data_path = tempfile.mkdtemp()\n        self.stopped = True\n        self.running = asyncio.Event()\n        self.block_expected = 0\n        self.mining_addr = ''\n\n    @property\n    def rpc_url(self):\n        # FIXME: somehow the hub/sdk doesn't learn the blocks through the Walet RPC port, why?\n        # return f'http://{self.rpcuser}:{self.rpcpassword}@{self.hostname}:{self.lbcwallet_rpcport}/'\n        return f'http://{self.rpcuser}:{self.rpcpassword}@{self.hostname}:{self.lbcd_rpcport}/'\n\n    def is_expected_block(self, e: BlockHeightEvent):\n        return self.block_expected == e.height\n\n    @property\n    def exists(self):\n        return (\n            os.path.exists(self.lbcwallet_bin)\n        )\n\n    def download(self):\n        uname = platform.uname()\n        target_os = str.lower(uname.system)\n        target_arch = str.replace(uname.machine, 'x86_64', 'amd64')\n        target_platform = target_os + '_' + target_arch\n        self.latest_release_url = str.replace(self.latest_release_url, 'TARGET_PLATFORM', target_platform)\n\n        downloaded_file = os.path.join(\n            self.bin_dir,\n            self.latest_release_url[self.latest_release_url.rfind('/')+1:]\n        )\n\n        if not os.path.exists(self.bin_dir):\n            os.mkdir(self.bin_dir)\n\n        if not os.path.exists(downloaded_file):\n            self.log.info('Downloading: %s', self.latest_release_url)\n            with urllib.request.urlopen(self.latest_release_url) as response:\n                with open(downloaded_file, 'wb') as out_file:\n                    shutil.copyfileobj(response, out_file)\n\n        self.log.info('Extracting: %s', downloaded_file)\n\n        if downloaded_file.endswith('.zip'):\n            with zipfile.ZipFile(downloaded_file) as dotzip:\n                dotzip.extractall(self.bin_dir)\n                # zipfile bug https://bugs.python.org/issue15795\n                os.chmod(self.lbcwallet_bin, 0o755)\n\n        elif downloaded_file.endswith('.tar.gz'):\n            with tarfile.open(downloaded_file) as tar:\n                tar.extractall(self.bin_dir)\n\n        return self.exists\n\n    def ensure(self):\n        return self.exists or self.download()\n\n    async def start(self):\n        assert self.ensure()\n        loop = asyncio.get_event_loop()\n        asyncio.get_child_watcher().attach_loop(loop)\n\n        command = [\n            self.lbcwallet_bin,\n            '--noservertls', '--noclienttls',\n            '--regtest',\n            f'--rpcconnect=127.0.0.1:{self.lbcd_rpcport}', f'--rpclisten=127.0.0.1:{self.lbcwallet_rpcport}',\n            '--createtemp', f'--appdata={self.data_path}',\n            f'--username={self.rpcuser}', f'--password={self.rpcpassword}'\n        ]\n        self.log.info(' '.join(command))\n        try:\n            self.transport, self.protocol = await loop.subprocess_exec(\n                WalletProcess, *command\n            )\n            self.protocol.transport = self.transport\n            await self.protocol.ready.wait()\n            assert not self.protocol.stopped.is_set()\n            self.running.set()\n            self.stopped = False\n        except asyncio.CancelledError:\n            self.running.clear()\n            raise\n        except Exception as e:\n            self.running.clear()\n            log.exception('failed to start lbcwallet', exc_info=e)\n\n    def cleanup(self):\n        assert self.stopped\n        shutil.rmtree(self.data_path, ignore_errors=True)\n\n    async def stop(self, cleanup=True):\n        if self.stopped:\n            return\n        try:\n            self.transport.terminate()\n            await self.protocol.stopped.wait()\n            self.transport.close()\n        except Exception as e:\n            log.exception('failed to stop lbcwallet', exc_info=e)\n            raise\n        finally:\n            self.log.info(\"Done shutting down \" + self.lbcwallet_bin)\n            self.stopped = True\n            if cleanup:\n                self.cleanup()\n            self.running.clear()\n\n    async def _cli_cmnd(self, *args):\n        cmnd_args = [\n            self.cli_bin,\n            f'--rpcuser={self.rpcuser}', f'--rpcpass={self.rpcpassword}', '--notls', '--regtest', '--wallet'\n        ] + list(args)\n        self.log.info(' '.join(cmnd_args))\n        loop = asyncio.get_event_loop()\n        asyncio.get_child_watcher().attach_loop(loop)\n        process = await asyncio.create_subprocess_exec(\n            *cmnd_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE\n        )\n        out, err = await process.communicate()\n        result = out.decode().strip()\n        err = err.decode().strip()\n        if len(result) <= 0 and err.startswith('-'):\n            raise Exception(err)\n        if err and 'creating a default config file' not in err:\n            log.warning(err)\n        self.log.info(result)\n        if result.startswith('error code'):\n            raise Exception(result)\n        return result\n\n    def generate(self, blocks):\n        self.block_expected += blocks\n        return self._cli_cmnd('generatetoaddress', str(blocks), self.mining_addr)\n\n    def generate_to_address(self, blocks, addr):\n        self.block_expected += blocks\n        return self._cli_cmnd('generatetoaddress', str(blocks), addr)\n\n    def wallet_passphrase(self, passphrase, timeout):\n        return self._cli_cmnd('walletpassphrase', passphrase, str(timeout))\n\n    def invalidate_block(self, blockhash):\n        return self._cli_cmnd('invalidateblock', blockhash)\n\n    def get_block_hash(self, block):\n        return self._cli_cmnd('getblockhash', str(block))\n\n    def sendrawtransaction(self, tx):\n        return self._cli_cmnd('sendrawtransaction', tx)\n\n    async def get_block(self, block_hash):\n        return json.loads(await self._cli_cmnd('getblock', block_hash, '1'))\n\n    def get_raw_change_address(self):\n        return self._cli_cmnd('getrawchangeaddress')\n\n    def get_new_address(self, address_type='legacy'):\n        return self._cli_cmnd('getnewaddress', \"\", address_type)\n\n    async def get_balance(self):\n        return await self._cli_cmnd('getbalance')\n\n    def send_to_address(self, address, amount):\n        return self._cli_cmnd('sendtoaddress', address, str(amount))\n\n    def send_raw_transaction(self, tx):\n        return self._cli_cmnd('sendrawtransaction', tx.decode())\n\n    def create_raw_transaction(self, inputs, outputs):\n        return self._cli_cmnd('createrawtransaction', json.dumps(inputs), json.dumps(outputs))\n\n    async def sign_raw_transaction_with_wallet(self, tx):\n        # the \"withwallet\" portion should only come into play if we are doing segwit.\n        # and \"withwallet\" doesn't exist on lbcd yet.\n        result = await self._cli_cmnd('signrawtransaction', tx)\n        return json.loads(result)['hex'].encode()\n\n    def decode_raw_transaction(self, tx):\n        return self._cli_cmnd('decoderawtransaction', hexlify(tx.raw).decode())\n\n    def get_raw_transaction(self, txid):\n        return self._cli_cmnd('getrawtransaction', txid, '1')\n"
  },
  {
    "path": "lbry/wallet/orchstr8/service.py",
    "content": "import asyncio\nimport logging\nfrom aiohttp.web import Application, WebSocketResponse, json_response\nfrom aiohttp.http_websocket import WSMsgType, WSCloseCode\n\nfrom lbry.wallet.util import satoshis_to_coins\nfrom .node import Conductor\n\n\nPORT = 7954\n\n\nclass WebSocketLogHandler(logging.Handler):\n\n    def __init__(self, send_message):\n        super().__init__()\n        self.send_message = send_message\n\n    def emit(self, record):\n        try:\n            self.send_message({\n                'type': 'log',\n                'name': record.name,\n                'message': self.format(record)\n            })\n        except Exception:\n            self.handleError(record)\n\n\nclass ConductorService:\n\n    def __init__(self, stack: Conductor, loop: asyncio.AbstractEventLoop) -> None:\n        self.stack = stack\n        self.loop = loop\n        self.app = Application()\n        self.app.router.add_post('/start', self.start_stack)\n        self.app.router.add_post('/generate', self.generate)\n        self.app.router.add_post('/transfer', self.transfer)\n        self.app.router.add_post('/balance', self.balance)\n        self.app.router.add_get('/log', self.log)\n        self.app['websockets'] = set()\n        self.app.on_shutdown.append(self.on_shutdown)\n        self.handler = self.app.make_handler()\n        self.server = None\n\n    async def start(self):\n        self.server = await self.loop.create_server(\n            self.handler, '0.0.0.0', PORT\n        )\n        print('serving on', self.server.sockets[0].getsockname())\n\n    async def stop(self):\n        await self.stack.stop()\n        self.server.close()\n        await self.server.wait_closed()\n        await self.app.shutdown()\n        await self.handler.shutdown(60.0)\n        await self.app.cleanup()\n\n    async def start_stack(self, _):\n        #set_logging(\n        #    self.stack.ledger_module, logging.DEBUG, WebSocketLogHandler(self.send_message)\n        #)\n        self.stack.lbcd_started or await self.stack.start_lbcd()\n        self.send_message({'type': 'service', 'name': 'lbcd', 'port': self.stack.lbcd_node.port})\n        self.stack.lbcwallet_started or await self.stack.start_lbcwallet()\n        self.send_message({'type': 'service', 'name': 'lbcwallet', 'port': self.stack.lbcwallet_node.port})\n        self.stack.spv_started or await self.stack.start_spv()\n        self.send_message({'type': 'service', 'name': 'spv', 'port': self.stack.spv_node.port})\n        self.stack.wallet_started or await self.stack.start_wallet()\n        self.send_message({'type': 'service', 'name': 'wallet', 'port': self.stack.wallet_node.port})\n        self.stack.wallet_node.ledger.on_header.listen(self.on_status)\n        self.stack.wallet_node.ledger.on_transaction.listen(self.on_status)\n        return json_response({'started': True})\n\n    async def generate(self, request):\n        data = await request.post()\n        blocks = data.get('blocks', 1)\n        await self.stack.lbcwallet_node.generate(int(blocks))\n        return json_response({'blocks': blocks})\n\n    async def transfer(self, request):\n        data = await request.post()\n        address = data.get('address')\n        if not address and self.stack.wallet_started:\n            address = await self.stack.wallet_node.account.receiving.get_or_create_usable_address()\n        if not address:\n            raise ValueError(\"No address was provided.\")\n        amount = data.get('amount', 1)\n        if self.stack.wallet_started:\n            watcher = self.stack.wallet_node.ledger.on_transaction.where(\n                lambda e: e.address == address  # and e.tx.id == txid -- might stall; see send_to_address_and_wait\n            )\n            txid = await self.stack.lbcwallet_node.send_to_address(address, amount)\n            await watcher\n        else:\n            txid = await self.stack.lbcwallet_node.send_to_address(address, amount)\n        return json_response({\n            'address': address,\n            'amount': amount,\n            'txid': txid\n        })\n\n    async def balance(self, _):\n        return json_response({\n            'balance': await self.stack.lbcwallet_node.get_balance()\n        })\n\n    async def log(self, request):\n        web_socket = WebSocketResponse()\n        await web_socket.prepare(request)\n        self.app['websockets'].add(web_socket)\n        try:\n            async for msg in web_socket:\n                if msg.type == WSMsgType.TEXT:\n                    if msg.data == 'close':\n                        await web_socket.close()\n                elif msg.type == WSMsgType.ERROR:\n                    print('web socket connection closed with exception %s' %\n                          web_socket.exception())\n        finally:\n            self.app['websockets'].remove(web_socket)\n        return web_socket\n\n    @staticmethod\n    async def on_shutdown(app):\n        for web_socket in app['websockets']:\n            await web_socket.close(code=WSCloseCode.GOING_AWAY, message='Server shutdown')\n\n    async def on_status(self, _):\n        if not self.app['websockets']:\n            return\n        self.send_message({\n            'type': 'status',\n            'height': self.stack.wallet_node.ledger.headers.height,\n            'balance': satoshis_to_coins(await self.stack.wallet_node.account.get_balance()),\n            'miner': await self.stack.lbcwallet_node.get_balance()\n        })\n\n    def send_message(self, msg):\n        for web_socket in self.app['websockets']:\n            self.loop.create_task(web_socket.send_json(msg))\n"
  },
  {
    "path": "lbry/wallet/rpc/__init__.py",
    "content": "from .framing import *\nfrom .jsonrpc import *\nfrom .socks import *\nfrom .session import *\nfrom .util import *\n\n__all__ = (framing.__all__ +\n           jsonrpc.__all__ +\n           socks.__all__ +\n           session.__all__ +\n           util.__all__)\n"
  },
  {
    "path": "lbry/wallet/rpc/framing.py",
    "content": "# Copyright (c) 2018, Neil Booth\n#\n# All rights reserved.\n#\n# The MIT License (MIT)\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n\"\"\"RPC message framing in a byte stream.\"\"\"\n\n__all__ = ('FramerBase', 'NewlineFramer', 'BinaryFramer', 'BitcoinFramer',\n           'OversizedPayloadError', 'BadChecksumError', 'BadMagicError')\n\nfrom hashlib import sha256 as _sha256\nfrom struct import Struct\nfrom asyncio import Queue\n\n\nclass FramerBase:\n    \"\"\"Abstract base class for a framer.\n\n    A framer breaks an incoming byte stream into protocol messages,\n    buffering if necessary.  It also frames outgoing messages into\n    a byte stream.\n    \"\"\"\n\n    def frame(self, message):\n        \"\"\"Return the framed message.\"\"\"\n        raise NotImplementedError\n\n    def received_bytes(self, data):\n        \"\"\"Pass incoming network bytes.\"\"\"\n        raise NotImplementedError\n\n    async def receive_message(self):\n        \"\"\"Wait for a complete unframed message to arrive, and return it.\"\"\"\n        raise NotImplementedError\n\n\nclass NewlineFramer(FramerBase):\n    \"\"\"A framer for a protocol where messages are separated by newlines.\"\"\"\n\n    # The default max_size value is motivated by JSONRPC, where a\n    # normal request will be 250 bytes or less, and a reasonable\n    # batch may contain 4000 requests.\n    def __init__(self, max_size=250 * 4000):\n        \"\"\"max_size - an anti-DoS measure.  If, after processing an incoming\n        message, buffered data would exceed max_size bytes, that\n        buffered data is dropped entirely and the framer waits for a\n        newline character to re-synchronize the stream.\n        \"\"\"\n        self.max_size = max_size\n        self.queue = Queue()\n        self.received_bytes = self.queue.put_nowait\n        self.synchronizing = False\n        self.residual = b''\n\n    def frame(self, message):\n        return message + b'\\n'\n\n    async def receive_message(self):\n        parts = []\n        buffer_size = 0\n        while True:\n            part = self.residual\n            self.residual = b''\n            if not part:\n                part = await self.queue.get()\n\n            npos = part.find(b'\\n')\n            if npos == -1:\n                parts.append(part)\n                buffer_size += len(part)\n                # Ignore over-sized messages; re-synchronize\n                if buffer_size <= self.max_size:\n                    continue\n                self.synchronizing = True\n                raise MemoryError(f'dropping message over {self.max_size:,d} '\n                                  f'bytes and re-synchronizing')\n\n            tail, self.residual = part[:npos], part[npos + 1:]\n            if self.synchronizing:\n                self.synchronizing = False\n                return await self.receive_message()\n            else:\n                parts.append(tail)\n                return b''.join(parts)\n\n\nclass ByteQueue:\n    \"\"\"A producer-comsumer queue.  Incoming network data is put as it\n    arrives, and the consumer calls an async method waiting for data of\n    a specific length.\"\"\"\n\n    def __init__(self):\n        self.queue = Queue()\n        self.parts = []\n        self.parts_len = 0\n        self.put_nowait = self.queue.put_nowait\n\n    async def receive(self, size):\n        while self.parts_len < size:\n            part = await self.queue.get()\n            self.parts.append(part)\n            self.parts_len += len(part)\n        self.parts_len -= size\n        whole = b''.join(self.parts)\n        self.parts = [whole[size:]]\n        return whole[:size]\n\n\nclass BinaryFramer:\n    \"\"\"A framer for binary messaging protocols.\"\"\"\n\n    def __init__(self):\n        self.byte_queue = ByteQueue()\n        self.message_queue = Queue()\n        self.received_bytes = self.byte_queue.put_nowait\n\n    def frame(self, message):\n        command, payload = message\n        return b''.join((\n            self._build_header(command, payload),\n            payload\n        ))\n\n    async def receive_message(self):\n        command, payload_len, checksum = await self._receive_header()\n        payload = await self.byte_queue.receive(payload_len)\n        payload_checksum = self._checksum(payload)\n        if payload_checksum != checksum:\n            raise BadChecksumError(payload_checksum, checksum)\n        return command, payload\n\n    def _checksum(self, payload):\n        raise NotImplementedError\n\n    def _build_header(self, command, payload):\n        raise NotImplementedError\n\n    async def _receive_header(self):\n        raise NotImplementedError\n\n\n# Helpers\nstruct_le_I = Struct('<I')\npack_le_uint32 = struct_le_I.pack\n\n\ndef sha256(x):\n    \"\"\"Simple wrapper of hashlib sha256.\"\"\"\n    return _sha256(x).digest()\n\n\ndef double_sha256(x):\n    \"\"\"SHA-256 of SHA-256, as used extensively in bitcoin.\"\"\"\n    return sha256(sha256(x))\n\n\nclass BadChecksumError(Exception):\n    pass\n\n\nclass BadMagicError(Exception):\n    pass\n\n\nclass OversizedPayloadError(Exception):\n    pass\n\n\nclass BitcoinFramer(BinaryFramer):\n    \"\"\"Provides a framer of binary message payloads in the style of the\n    Bitcoin network protocol.\n\n    Each binary message has the following elements, in order:\n\n       Magic    - to confirm network (currently unused for stream sync)\n       Command  - padded command\n       Length   - payload length in bytes\n       Checksum - checksum of the payload\n       Payload  - binary payload\n\n    Call frame(command, payload) to get a framed message.\n    Pass incoming network bytes to received_bytes().\n    Wait on receive_message() to get incoming (command, payload) pairs.\n    \"\"\"\n\n    def __init__(self, magic, max_block_size):\n        def pad_command(command):\n            fill = 12 - len(command)\n            if fill < 0:\n                raise ValueError(f'command {command} too long')\n            return command + bytes(fill)\n\n        super().__init__()\n        self._magic = magic\n        self._max_block_size = max_block_size\n        self._pad_command = pad_command\n        self._unpack = Struct(f'<4s12sI4s').unpack\n\n    def _checksum(self, payload):\n        return double_sha256(payload)[:4]\n\n    def _build_header(self, command, payload):\n        return b''.join((\n            self._magic,\n            self._pad_command(command),\n            pack_le_uint32(len(payload)),\n            self._checksum(payload)\n        ))\n\n    async def _receive_header(self):\n        header = await self.byte_queue.receive(24)\n        magic, command, payload_len, checksum = self._unpack(header)\n        if magic != self._magic:\n            raise BadMagicError(magic, self._magic)\n        command = command.rstrip(b'\\0')\n        if payload_len > 1024 * 1024:\n            if command != b'block' or payload_len > self._max_block_size:\n                raise OversizedPayloadError(command, payload_len)\n        return command, payload_len, checksum\n"
  },
  {
    "path": "lbry/wallet/rpc/jsonrpc.py",
    "content": "# Copyright (c) 2018, Neil Booth\n#\n# All rights reserved.\n#\n# The MIT License (MIT)\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n\"\"\"Classes for JSONRPC versions 1.0 and 2.0, and a loose interpretation.\"\"\"\n\n__all__ = ('JSONRPC', 'JSONRPCv1', 'JSONRPCv2', 'JSONRPCLoose',\n           'JSONRPCAutoDetect', 'Request', 'Notification', 'Batch',\n           'RPCError', 'ProtocolError',\n           'JSONRPCConnection', 'handler_invocation')\n\nimport itertools\nimport json\nimport typing\nimport asyncio\nfrom functools import partial\nfrom numbers import Number\n\nimport attr\nfrom asyncio import Queue, Event, CancelledError\nfrom .util import signature_info\n\n\nclass SingleRequest:\n    __slots__ = ('method', 'args')\n\n    def __init__(self, method, args):\n        if not isinstance(method, str):\n            raise ProtocolError(JSONRPC.METHOD_NOT_FOUND,\n                                'method must be a string')\n        if not isinstance(args, (list, tuple, dict)):\n            raise ProtocolError.invalid_args('request arguments must be a '\n                                             'list or a dictionary')\n        self.args = args\n        self.method = method\n\n    def __repr__(self):\n        return f'{self.__class__.__name__}({self.method!r}, {self.args!r})'\n\n    def __eq__(self, other):\n        return (isinstance(other, self.__class__) and\n                self.method == other.method and self.args == other.args)\n\n\nclass Request(SingleRequest):\n    def send_result(self, response):\n        return None\n\n\nclass Notification(SingleRequest):\n    pass\n\n\nclass Batch:\n    __slots__ = ('items', )\n\n    def __init__(self, items):\n        if not isinstance(items, (list, tuple)):\n            raise ProtocolError.invalid_request('items must be a list')\n        if not items:\n            raise ProtocolError.empty_batch()\n        if not (all(isinstance(item, SingleRequest) for item in items) or\n                all(isinstance(item, Response) for item in items)):\n            raise ProtocolError.invalid_request('batch must be homogeneous')\n        self.items = items\n\n    def __len__(self):\n        return len(self.items)\n\n    def __getitem__(self, item):\n        return self.items[item]\n\n    def __iter__(self):\n        return iter(self.items)\n\n    def __repr__(self):\n        return f'Batch({len(self.items)} items)'\n\n\nclass Response:\n    __slots__ = ('result', )\n\n    def __init__(self, result):\n        # Type checking happens when converting to a message\n        self.result = result\n\n\nclass CodeMessageError(Exception):\n\n    @property\n    def code(self):\n        return self.args[0]\n\n    @property\n    def message(self):\n        return self.args[1]\n\n    def __eq__(self, other):\n        return (isinstance(other, self.__class__) and\n                self.code == other.code and self.message == other.message)\n\n    def __hash__(self):\n        # overridden to make the exception hashable\n        # see https://bugs.python.org/issue28603\n        return hash((self.code, self.message))\n\n    @classmethod\n    def invalid_args(cls, message):\n        return cls(JSONRPC.INVALID_ARGS, message)\n\n    @classmethod\n    def invalid_request(cls, message):\n        return cls(JSONRPC.INVALID_REQUEST, message)\n\n    @classmethod\n    def empty_batch(cls):\n        return cls.invalid_request('batch is empty')\n\n\nclass RPCError(CodeMessageError):\n    pass\n\n\nclass ProtocolError(CodeMessageError):\n\n    def __init__(self, code, message):\n        super().__init__(code, message)\n        # If not None send this unframed message over the network\n        self.error_message = None\n        # If the error was in a JSON response message; its message ID.\n        # Since None can be a response message ID, \"id\" means the\n        # error was not sent in a JSON response\n        self.response_msg_id = id\n\n\nclass JSONRPC:\n    \"\"\"Abstract base class that interprets and constructs JSON RPC messages.\"\"\"\n\n    # Error codes.  See http://www.jsonrpc.org/specification\n    PARSE_ERROR = -32700\n    INVALID_REQUEST = -32600\n    METHOD_NOT_FOUND = -32601\n    INVALID_ARGS = -32602\n    INTERNAL_ERROR = -32603\n    QUERY_TIMEOUT = -32000\n\n    # Codes specific to this library\n    ERROR_CODE_UNAVAILABLE = -100\n\n    # Can be overridden by derived classes\n    allow_batches = True\n\n    @classmethod\n    def _message_id(cls, message, require_id):\n        \"\"\"Validate the message is a dictionary and return its ID.\n\n        Raise an error if the message is invalid or the ID is of an\n        invalid type.  If it has no ID, raise an error if require_id\n        is True, otherwise return None.\n        \"\"\"\n        raise NotImplementedError\n\n    @classmethod\n    def _validate_message(cls, message):\n        \"\"\"Validate other parts of the message other than those\n        done in _message_id.\"\"\"\n        pass\n\n    @classmethod\n    def _request_args(cls, request):\n        \"\"\"Validate the existence and type of the arguments passed\n        in the request dictionary.\"\"\"\n        raise NotImplementedError\n\n    @classmethod\n    def _process_request(cls, payload):\n        request_id = None\n        try:\n            request_id = cls._message_id(payload, False)\n            cls._validate_message(payload)\n            method = payload.get('method')\n            if request_id is None:\n                item = Notification(method, cls._request_args(payload))\n            else:\n                item = Request(method, cls._request_args(payload))\n            return item, request_id\n        except ProtocolError as error:\n            code, message = error.code, error.message\n        raise cls._error(code, message, True, request_id)\n\n    @classmethod\n    def _process_response(cls, payload):\n        request_id = None\n        try:\n            request_id = cls._message_id(payload, True)\n            cls._validate_message(payload)\n            return Response(cls.response_value(payload)), request_id\n        except ProtocolError as error:\n            code, message = error.code, error.message\n        raise cls._error(code, message, False, request_id)\n\n    @classmethod\n    def _message_to_payload(cls, message):\n        \"\"\"Returns a Python object or a ProtocolError.\"\"\"\n        try:\n            return json.loads(message.decode())\n        except UnicodeDecodeError:\n            message = 'messages must be encoded in UTF-8'\n        except json.JSONDecodeError:\n            message = 'invalid JSON'\n        raise cls._error(cls.PARSE_ERROR, message, True, None)\n\n    @classmethod\n    def _error(cls, code, message, send, msg_id):\n        error = ProtocolError(code, message)\n        if send:\n            error.error_message = cls.response_message(error, msg_id)\n        else:\n            error.response_msg_id = msg_id\n        return error\n\n    #\n    # External API\n    #\n\n    @classmethod\n    def message_to_item(cls, message):\n        \"\"\"Translate an unframed received message and return an\n        (item, request_id) pair.\n\n        The item can be a Request, Notification, Response or a list.\n\n        A JSON RPC error response is returned as an RPCError inside a\n        Response object.\n\n        If a Batch is returned, request_id is an iterable of request\n        ids, one per batch member.\n\n        If the message violates the protocol in some way a\n        ProtocolError is returned, except if the message was\n        determined to be a response, in which case the ProtocolError\n        is placed inside a Response object.  This is so that client\n        code can mark a request as having been responded to even if\n        the response was bad.\n\n        raises: ProtocolError\n        \"\"\"\n        payload = cls._message_to_payload(message)\n        if isinstance(payload, dict):\n            if 'method' in payload:\n                return cls._process_request(payload)\n            else:\n                return cls._process_response(payload)\n        elif isinstance(payload, list) and cls.allow_batches:\n            if not payload:\n                raise cls._error(JSONRPC.INVALID_REQUEST, 'batch is empty',\n                                 True, None)\n            return payload, None\n        raise cls._error(cls.INVALID_REQUEST,\n                         'request object must be a dictionary', True, None)\n\n    # Message formation\n    @classmethod\n    def request_message(cls, item, request_id):\n        \"\"\"Convert an RPCRequest item to a message.\"\"\"\n        assert isinstance(item, Request)\n        return cls.encode_payload(cls.request_payload(item, request_id))\n\n    @classmethod\n    def notification_message(cls, item):\n        \"\"\"Convert an RPCRequest item to a message.\"\"\"\n        assert isinstance(item, Notification)\n        return cls.encode_payload(cls.request_payload(item, None))\n\n    @classmethod\n    def response_message(cls, result, request_id):\n        \"\"\"Convert a response result (or RPCError) to a message.\"\"\"\n        if isinstance(result, CodeMessageError):\n            payload = cls.error_payload(result, request_id)\n        else:\n            payload = cls.response_payload(result, request_id)\n        return cls.encode_payload(payload)\n\n    @classmethod\n    def batch_message(cls, batch, request_ids):\n        \"\"\"Convert a request Batch to a message.\"\"\"\n        assert isinstance(batch, Batch)\n        if not cls.allow_batches:\n            raise ProtocolError.invalid_request(\n                'protocol does not permit batches')\n        id_iter = iter(request_ids)\n        rm = cls.request_message\n        nm = cls.notification_message\n        parts = (rm(request, next(id_iter)) if isinstance(request, Request)\n                 else nm(request) for request in batch)\n        return cls.batch_message_from_parts(parts)\n\n    @classmethod\n    def batch_message_from_parts(cls, messages):\n        \"\"\"Convert messages, one per batch item, into a batch message.  At\n        least one message must be passed.\n        \"\"\"\n        # Comma-separate the messages and wrap the lot in square brackets\n        middle = b', '.join(messages)\n        if not middle:\n            raise ProtocolError.empty_batch()\n        return b''.join([b'[', middle, b']'])\n\n    @classmethod\n    def encode_payload(cls, payload):\n        \"\"\"Encode a Python object as JSON and convert it to bytes.\"\"\"\n        try:\n            return json.dumps(payload).encode()\n        except TypeError:\n            msg = f'JSON payload encoding error: {payload}'\n            raise ProtocolError(cls.INTERNAL_ERROR, msg) from None\n\n\nclass JSONRPCv1(JSONRPC):\n    \"\"\"JSON RPC version 1.0.\"\"\"\n\n    allow_batches = False\n\n    @classmethod\n    def _message_id(cls, message, require_id):\n        # JSONv1 requires an ID always, but without constraint on its type\n        # No need to test for a dictionary here as we don't handle batches.\n        if 'id' not in message:\n            raise ProtocolError.invalid_request('request has no \"id\"')\n        return message['id']\n\n    @classmethod\n    def _request_args(cls, request):\n        args = request.get('params')\n        if not isinstance(args, list):\n            raise ProtocolError.invalid_args(\n                f'invalid request arguments: {args}')\n        return args\n\n    @classmethod\n    def _best_effort_error(cls, error):\n        # Do our best to interpret the error\n        code = cls.ERROR_CODE_UNAVAILABLE\n        message = 'no error message provided'\n        if isinstance(error, str):\n            message = error\n        elif isinstance(error, int):\n            code = error\n        elif isinstance(error, dict):\n            if isinstance(error.get('message'), str):\n                message = error['message']\n            if isinstance(error.get('code'), int):\n                code = error['code']\n\n        return RPCError(code, message)\n\n    @classmethod\n    def response_value(cls, payload):\n        if 'result' not in payload or 'error' not in payload:\n            raise ProtocolError.invalid_request(\n                'response must contain both \"result\" and \"error\"')\n\n        result = payload['result']\n        error = payload['error']\n        if error is None:\n            return result   # It seems None can be a valid result\n        if result is not None:\n            raise ProtocolError.invalid_request(\n                'response has a \"result\" and an \"error\"')\n\n        return cls._best_effort_error(error)\n\n    @classmethod\n    def request_payload(cls, request, request_id):\n        \"\"\"JSON v1 request (or notification) payload.\"\"\"\n        if isinstance(request.args, dict):\n            raise ProtocolError.invalid_args(\n                'JSONRPCv1 does not support named arguments')\n        return {\n            'method': request.method,\n            'params': request.args,\n            'id': request_id\n        }\n\n    @classmethod\n    def response_payload(cls, result, request_id):\n        \"\"\"JSON v1 response payload.\"\"\"\n        return {\n            'result': result,\n            'error': None,\n            'id': request_id\n        }\n\n    @classmethod\n    def error_payload(cls, error, request_id):\n        return {\n            'result': None,\n            'error': {'code': error.code, 'message': error.message},\n            'id': request_id\n        }\n\n\nclass JSONRPCv2(JSONRPC):\n    \"\"\"JSON RPC version 2.0.\"\"\"\n\n    @classmethod\n    def _message_id(cls, message, require_id):\n        if not isinstance(message, dict):\n            raise ProtocolError.invalid_request(\n                'request object must be a dictionary')\n        if 'id' in message:\n            request_id = message['id']\n            if not isinstance(request_id, (Number, str, type(None))):\n                raise ProtocolError.invalid_request(\n                    f'invalid \"id\": {request_id}')\n            return request_id\n        else:\n            if require_id:\n                raise ProtocolError.invalid_request('request has no \"id\"')\n            return None\n\n    @classmethod\n    def _validate_message(cls, message):\n        if message.get('jsonrpc') != '2.0':\n            raise ProtocolError.invalid_request('\"jsonrpc\" is not \"2.0\"')\n\n    @classmethod\n    def _request_args(cls, request):\n        args = request.get('params', [])\n        if not isinstance(args, (dict, list)):\n            raise ProtocolError.invalid_args(\n                f'invalid request arguments: {args}')\n        return args\n\n    @classmethod\n    def response_value(cls, payload):\n        if 'result' in payload:\n            if 'error' in payload:\n                raise ProtocolError.invalid_request(\n                    'response contains both \"result\" and \"error\"')\n            return payload['result']\n\n        if 'error' not in payload:\n            raise ProtocolError.invalid_request(\n                'response contains neither \"result\" nor \"error\"')\n\n        # Return an RPCError object\n        error = payload['error']\n        if isinstance(error, dict):\n            code = error.get('code')\n            message = error.get('message')\n            if isinstance(code, int) and isinstance(message, str):\n                return RPCError(code, message)\n\n        raise ProtocolError.invalid_request(\n            f'ill-formed response error object: {error}')\n\n    @classmethod\n    def request_payload(cls, request, request_id):\n        \"\"\"JSON v2 request (or notification) payload.\"\"\"\n        payload = {\n            'jsonrpc': '2.0',\n            'method': request.method,\n        }\n        # A notification?\n        if request_id is not None:\n            payload['id'] = request_id\n        # Preserve empty dicts as missing params is read as an array\n        if request.args or request.args == {}:\n            payload['params'] = request.args\n        return payload\n\n    @classmethod\n    def response_payload(cls, result, request_id):\n        \"\"\"JSON v2 response payload.\"\"\"\n        return {\n            'jsonrpc': '2.0',\n            'result': result,\n            'id': request_id\n        }\n\n    @classmethod\n    def error_payload(cls, error, request_id):\n        return {\n            'jsonrpc': '2.0',\n            'error': {'code': error.code, 'message': error.message},\n            'id': request_id\n        }\n\n\nclass JSONRPCLoose(JSONRPC):\n    \"\"\"A relaxed version of JSON RPC.\"\"\"\n\n    # Don't be so loose we accept any old message ID\n    _message_id = JSONRPCv2._message_id\n    _validate_message = JSONRPC._validate_message\n    _request_args = JSONRPCv2._request_args\n    # Outoing messages are JSONRPCv2 so we give the other side the\n    # best chance to assume / detect JSONRPCv2 as default protocol.\n    error_payload = JSONRPCv2.error_payload\n    request_payload = JSONRPCv2.request_payload\n    response_payload = JSONRPCv2.response_payload\n\n    @classmethod\n    def response_value(cls, payload):\n        # Return result, unless it is None and there is an error\n        if payload.get('error') is not None:\n            if payload.get('result') is not None:\n                raise ProtocolError.invalid_request(\n                    'response contains both \"result\" and \"error\"')\n            return JSONRPCv1._best_effort_error(payload['error'])\n\n        if 'result' not in payload:\n            raise ProtocolError.invalid_request(\n                'response contains neither \"result\" nor \"error\"')\n\n        # Can be None\n        return payload['result']\n\n\nclass JSONRPCAutoDetect(JSONRPCv2):\n\n    @classmethod\n    def message_to_item(cls, message):\n        return cls.detect_protocol(message), None\n\n    @classmethod\n    def detect_protocol(cls, message):\n        \"\"\"Attempt to detect the protocol from the message.\"\"\"\n        main = cls._message_to_payload(message)\n\n        def protocol_for_payload(payload):\n            if not isinstance(payload, dict):\n                return JSONRPCLoose   # Will error\n            # Obey an explicit \"jsonrpc\"\n            version = payload.get('jsonrpc')\n            if version == '2.0':\n                return JSONRPCv2\n            if version == '1.0':\n                return JSONRPCv1\n\n            # Now to decide between JSONRPCLoose and JSONRPCv1 if possible\n            if 'result' in payload and 'error' in payload:\n                return JSONRPCv1\n            return JSONRPCLoose\n\n        if isinstance(main, list):\n            parts = {protocol_for_payload(payload) for payload in main}\n            # If all same protocol, return it\n            if len(parts) == 1:\n                return parts.pop()\n            # If strict protocol detected, return it, preferring JSONRPCv2.\n            # This means a batch of JSONRPCv1 will fail\n            for protocol in (JSONRPCv2, JSONRPCv1):\n                if protocol in parts:\n                    return protocol\n            # Will error if no parts\n            return JSONRPCLoose\n\n        return protocol_for_payload(main)\n\n\nclass JSONRPCConnection:\n    \"\"\"Maintains state of a JSON RPC connection, in particular\n    encapsulating the handling of request IDs.\n\n    protocol - the JSON RPC protocol to follow\n    max_response_size - responses over this size send an error response\n        instead.\n    \"\"\"\n\n    _id_counter = itertools.count()\n\n    def __init__(self, protocol):\n        self._protocol = protocol\n        # Sent Requests and Batches that have not received a response.\n        # The key is its request ID; for a batch it is sorted tuple\n        # of request IDs\n        self._requests: typing.Dict[str, typing.Tuple[Request, Event]] = {}\n        # A public attribute intended to be settable dynamically\n        self.max_response_size = 0\n\n    def _oversized_response_message(self, request_id):\n        text = f'response too large (over {self.max_response_size:,d} bytes'\n        error = RPCError.invalid_request(text)\n        return self._protocol.response_message(error, request_id)\n\n    def _receive_response(self, result, request_id):\n        if request_id not in self._requests:\n            if request_id is None and isinstance(result, RPCError):\n                message = f'diagnostic error received: {result}'\n            else:\n                message = f'response to unsent request (ID: {request_id})'\n            raise ProtocolError.invalid_request(message) from None\n        request, event = self._requests.pop(request_id)\n        event.result = result\n        event.set()\n        return []\n\n    def _receive_request_batch(self, payloads):\n        def item_send_result(request_id, result):\n            nonlocal size\n            part = protocol.response_message(result, request_id)\n            size += len(part) + 2\n            if size > self.max_response_size > 0:\n                part = self._oversized_response_message(request_id)\n            parts.append(part)\n            if len(parts) == count:\n                return protocol.batch_message_from_parts(parts)\n            return None\n\n        parts = []\n        items = []\n        size = 0\n        count = 0\n        protocol = self._protocol\n        for payload in payloads:\n            try:\n                item, request_id = protocol._process_request(payload)\n                items.append(item)\n                if isinstance(item, Request):\n                    count += 1\n                    item.send_result = partial(item_send_result, request_id)\n            except ProtocolError as error:\n                count += 1\n                parts.append(error.error_message)\n\n        if not items and parts:\n            protocol_error = ProtocolError(0, \"\")\n            protocol_error.error_message = protocol.batch_message_from_parts(parts)\n            raise protocol_error\n        return items\n\n    def _receive_response_batch(self, payloads):\n        request_ids = []\n        results = []\n        for payload in payloads:\n            # Let ProtocolError exceptions through\n            item, request_id = self._protocol._process_response(payload)\n            request_ids.append(request_id)\n            results.append(item.result)\n\n        ordered = sorted(zip(request_ids, results), key=lambda t: t[0])\n        ordered_ids, ordered_results = zip(*ordered)\n        if ordered_ids not in self._requests:\n            raise ProtocolError.invalid_request('response to unsent batch')\n        request_batch, event = self._requests.pop(ordered_ids)\n        event.result = ordered_results\n        event.set()\n        return []\n\n    def _send_result(self, request_id, result):\n        message = self._protocol.response_message(result, request_id)\n        if len(message) > self.max_response_size > 0:\n            message = self._oversized_response_message(request_id)\n        return message\n\n    def _event(self, request, request_id):\n        event = Event()\n        self._requests[request_id] = (request, event)\n        return event\n\n    #\n    # External API\n    #\n    def send_request(self, request: Request) -> typing.Tuple[bytes, Event]:\n        \"\"\"Send a Request.  Return a (message, event) pair.\n\n        The message is an unframed message to send over the network.\n        Wait on the event for the response; which will be in the\n        \"result\" attribute.\n\n        Raises: ProtocolError if the request violates the protocol\n        in some way..\n        \"\"\"\n        request_id = next(self._id_counter)\n        message = self._protocol.request_message(request, request_id)\n        return message, self._event(request, request_id)\n\n    def send_notification(self, notification):\n        return self._protocol.notification_message(notification)\n\n    def send_batch(self, batch):\n        ids = tuple(next(self._id_counter)\n                    for request in batch if isinstance(request, Request))\n        message = self._protocol.batch_message(batch, ids)\n        event = self._event(batch, ids) if ids else None\n        return message, event\n\n    def receive_message(self, message):\n        \"\"\"Call with an unframed message received from the network.\n\n        Raises: ProtocolError if the message violates the protocol in\n        some way.  However, if it happened in a response that can be\n        paired with a request, the ProtocolError is instead set in the\n        result attribute of the send_request() that caused the error.\n        \"\"\"\n        try:\n            item, request_id = self._protocol.message_to_item(message)\n        except ProtocolError as e:\n            if e.response_msg_id is not id:\n                return self._receive_response(e, e.response_msg_id)\n            raise\n\n        if isinstance(item, Request):\n            item.send_result = partial(self._send_result, request_id)\n            return [item]\n        if isinstance(item, Notification):\n            return [item]\n        if isinstance(item, Response):\n            return self._receive_response(item.result, request_id)\n        if isinstance(item, list):\n            if all(isinstance(payload, dict)\n                   and ('result' in payload or 'error' in payload)\n                   for payload in item):\n                return self._receive_response_batch(item)\n            else:\n                return self._receive_request_batch(item)\n        else:\n            # Protocol auto-detection hack\n            assert issubclass(item, JSONRPC)\n            self._protocol = item\n            return self.receive_message(message)\n\n    def raise_pending_requests(self, exception):\n        exception = exception or asyncio.TimeoutError()\n        for request, event in self._requests.values():\n            event.result = exception\n            event.set()\n        self._requests.clear()\n\n    def pending_requests(self):\n        \"\"\"All sent requests that have not received a response.\"\"\"\n        return [request for request, event in self._requests.values()]\n\n\ndef handler_invocation(handler, request):\n    method, args = request.method, request.args\n    if handler is None:\n        raise RPCError(JSONRPC.METHOD_NOT_FOUND,\n                       f'unknown method \"{method}\"')\n\n    # We must test for too few and too many arguments.  How\n    # depends on whether the arguments were passed as a list or as\n    # a dictionary.\n    info = signature_info(handler)\n    if isinstance(args, (tuple, list)):\n        if len(args) < info.min_args:\n            s = '' if len(args) == 1 else 's'\n            raise RPCError.invalid_args(\n                f'{len(args)} argument{s} passed to method '\n                f'\"{method}\" but it requires {info.min_args}')\n        if info.max_args is not None and len(args) > info.max_args:\n            s = '' if len(args) == 1 else 's'\n            raise RPCError.invalid_args(\n                f'{len(args)} argument{s} passed to method '\n                f'{method} taking at most {info.max_args}')\n        return partial(handler, *args)\n\n    # Arguments passed by name\n    if info.other_names is None:\n        raise RPCError.invalid_args(f'method \"{method}\" cannot '\n                                    f'be called with named arguments')\n\n    missing = set(info.required_names).difference(args)\n    if missing:\n        s = '' if len(missing) == 1 else 's'\n        missing = ', '.join(sorted(f'\"{name}\"' for name in missing))\n        raise RPCError.invalid_args(f'method \"{method}\" requires '\n                                    f'parameter{s} {missing}')\n\n    if info.other_names is not any:\n        excess = set(args).difference(info.required_names)\n        excess = excess.difference(info.other_names)\n        if excess:\n            s = '' if len(excess) == 1 else 's'\n            excess = ', '.join(sorted(f'\"{name}\"' for name in excess))\n            raise RPCError.invalid_args(f'method \"{method}\" does not '\n                                        f'take parameter{s} {excess}')\n    return partial(handler, **args)\n"
  },
  {
    "path": "lbry/wallet/rpc/session.py",
    "content": "# Copyright (c) 2018, Neil Booth\n#\n# All rights reserved.\n#\n# The MIT License (MIT)\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n\n__all__ = ('Connector', 'RPCSession', 'MessageSession', 'Server',\n           'BatchError')\n\n\nimport asyncio\nfrom asyncio import Event, CancelledError\nimport logging\nimport time\nfrom contextlib import suppress\nfrom prometheus_client import Counter, Histogram\nfrom lbry.wallet.tasks import TaskGroup\n\nfrom .jsonrpc import Request, JSONRPCConnection, JSONRPCv2, JSONRPC, Batch, Notification\nfrom .jsonrpc import RPCError, ProtocolError\nfrom .framing import BadMagicError, BadChecksumError, OversizedPayloadError, BitcoinFramer, NewlineFramer\n\nHISTOGRAM_BUCKETS = (\n    .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')\n)\n\n\nclass Connector:\n\n    def __init__(self, session_factory, host=None, port=None, proxy=None,\n                 **kwargs):\n        self.session_factory = session_factory\n        self.host = host\n        self.port = port\n        self.proxy = proxy\n        self.loop = kwargs.get('loop', asyncio.get_event_loop())\n        self.kwargs = kwargs\n\n    async def create_connection(self):\n        \"\"\"Initiate a connection.\"\"\"\n        connector = self.proxy or self.loop\n        return await connector.create_connection(\n            self.session_factory, self.host, self.port, **self.kwargs)\n\n    async def __aenter__(self):\n        transport, self.protocol = await self.create_connection()\n        return self.protocol\n\n    async def __aexit__(self, exc_type, exc_value, traceback):\n        await self.protocol.close()\n\n\nclass SessionBase(asyncio.Protocol):\n    \"\"\"Base class of networking sessions.\n\n    There is no client / server distinction other than who initiated\n    the connection.\n\n    To initiate a connection to a remote server pass host, port and\n    proxy to the constructor, and then call create_connection().  Each\n    successful call should have a corresponding call to close().\n\n    Alternatively if used in a with statement, the connection is made\n    on entry to the block, and closed on exit from the block.\n    \"\"\"\n\n    max_errors = 10\n\n    def __init__(self, *, framer=None, loop=None):\n        self.framer = framer or self.default_framer()\n        self.loop = loop or asyncio.get_event_loop()\n        self.logger = logging.getLogger(self.__class__.__name__)\n        self.transport = None\n        # Set when a connection is made\n        self._address = None\n        self._proxy_address = None\n        # For logger.debug messages\n        self.verbosity = 0\n        # Cleared when the send socket is full\n        self._can_send = Event()\n        self._can_send.set()\n        self._pm_task = None\n        self._task_group = TaskGroup(self.loop)\n        # Force-close a connection if a send doesn't succeed in this time\n        self.max_send_delay = 60\n        # Statistics.  The RPC object also keeps its own statistics.\n        self.start_time = time.perf_counter()\n        self.errors = 0\n        self.send_count = 0\n        self.send_size = 0\n        self.last_send = self.start_time\n        self.recv_count = 0\n        self.recv_size = 0\n        self.last_recv = self.start_time\n        self.last_packet_received = self.start_time\n\n    async def _limited_wait(self, secs):\n        try:\n            await asyncio.wait_for(self._can_send.wait(), secs)\n        except asyncio.TimeoutError:\n            self.abort()\n            raise asyncio.TimeoutError(f'task timed out after {secs}s')\n\n    async def _send_message(self, message):\n        if not self._can_send.is_set():\n            await self._limited_wait(self.max_send_delay)\n        if not self.is_closing():\n            framed_message = self.framer.frame(message)\n            self.send_size += len(framed_message)\n            self.send_count += 1\n            self.last_send = time.perf_counter()\n            if self.verbosity >= 4:\n                self.logger.debug(f'Sending framed message {framed_message}')\n            self.transport.write(framed_message)\n\n    def _bump_errors(self):\n        self.errors += 1\n        if self.errors >= self.max_errors:\n            # Don't await self.close() because that is self-cancelling\n            self._close()\n\n    def _close(self):\n        if self.transport:\n            self.transport.close()\n\n    # asyncio framework\n    def data_received(self, framed_message):\n        \"\"\"Called by asyncio when a message comes in.\"\"\"\n        self.last_packet_received = time.perf_counter()\n        if self.verbosity >= 4:\n            self.logger.debug(f'Received framed message {framed_message}')\n        self.recv_size += len(framed_message)\n        self.framer.received_bytes(framed_message)\n\n    def pause_writing(self):\n        \"\"\"Transport calls when the send buffer is full.\"\"\"\n        if not self.is_closing():\n            self._can_send.clear()\n            self.transport.pause_reading()\n\n    def resume_writing(self):\n        \"\"\"Transport calls when the send buffer has room.\"\"\"\n        if not self._can_send.is_set():\n            self._can_send.set()\n            self.transport.resume_reading()\n\n    def connection_made(self, transport):\n        \"\"\"Called by asyncio when a connection is established.\n\n        Derived classes overriding this method must call this first.\"\"\"\n        self.transport = transport\n        # This would throw if called on a closed SSL transport.  Fixed\n        # in asyncio in Python 3.6.1 and 3.5.4\n        peer_address = transport.get_extra_info('peername')\n        # If the Socks proxy was used then _address is already set to\n        # the remote address\n        if self._address:\n            self._proxy_address = peer_address\n        else:\n            self._address = peer_address\n        self._pm_task = self.loop.create_task(self._receive_messages())\n\n    def connection_lost(self, exc):\n        \"\"\"Called by asyncio when the connection closes.\n\n        Tear down things done in connection_made.\"\"\"\n        self._address = None\n        self.transport = None\n        self._task_group.cancel()\n        if self._pm_task:\n            self._pm_task.cancel()\n        # Release waiting tasks\n        self._can_send.set()\n\n    # External API\n    def default_framer(self):\n        \"\"\"Return a default framer.\"\"\"\n        raise NotImplementedError\n\n    def peer_address(self):\n        \"\"\"Returns the peer's address (Python networking address), or None if\n        no connection or an error.\n\n        This is the result of socket.getpeername() when the connection\n        was made.\n        \"\"\"\n        return self._address\n\n    def peer_address_str(self, for_log=True):\n        \"\"\"Returns the peer's IP address and port as a human-readable\n        string.\"\"\"\n        if not self._address:\n            return 'unknown'\n        ip_addr_str, port = self._address[:2]\n        if ':' in ip_addr_str:\n            return f'[{ip_addr_str}]:{port}'\n        else:\n            return f'{ip_addr_str}:{port}'\n\n    def is_closing(self):\n        \"\"\"Return True if the connection is closing.\"\"\"\n        return not self.transport or self.transport.is_closing()\n\n    def abort(self):\n        \"\"\"Forcefully close the connection.\"\"\"\n        if self.transport:\n            self.transport.abort()\n\n    # TODO: replace with synchronous_close\n    async def close(self, *, force_after=30):\n        \"\"\"Close the connection and return when closed.\"\"\"\n        self._close()\n        if self._pm_task:\n            with suppress(CancelledError):\n                await asyncio.wait([self._pm_task], timeout=force_after)\n                self.abort()\n                await self._pm_task\n\n    def synchronous_close(self):\n        self._close()\n        if self._pm_task and not self._pm_task.done():\n            self._pm_task.cancel()\n\n\nclass MessageSession(SessionBase):\n    \"\"\"Session class for protocols where messages are not tied to responses,\n    such as the Bitcoin protocol.\n\n    To use as a client (connection-opening) session, pass host, port\n    and perhaps a proxy.\n    \"\"\"\n    async def _receive_messages(self):\n        while not self.is_closing():\n            try:\n                message = await self.framer.receive_message()\n            except BadMagicError as e:\n                magic, expected = e.args\n                self.logger.error(\n                    f'bad network magic: got {magic} expected {expected}, '\n                    f'disconnecting'\n                )\n                self._close()\n            except OversizedPayloadError as e:\n                command, payload_len = e.args\n                self.logger.error(\n                    f'oversized payload of {payload_len:,d} bytes to command '\n                    f'{command}, disconnecting'\n                )\n                self._close()\n            except BadChecksumError as e:\n                payload_checksum, claimed_checksum = e.args\n                self.logger.warning(\n                    f'checksum mismatch: actual {payload_checksum.hex()} '\n                    f'vs claimed {claimed_checksum.hex()}'\n                )\n                self._bump_errors()\n            else:\n                self.last_recv = time.perf_counter()\n                self.recv_count += 1\n                await self._task_group.add(self._handle_message(message))\n\n    async def _handle_message(self, message):\n        try:\n            await self.handle_message(message)\n        except ProtocolError as e:\n            self.logger.error(f'{e}')\n            self._bump_errors()\n        except CancelledError:\n            raise\n        except Exception:\n            self.logger.exception(f'exception handling {message}')\n            self._bump_errors()\n\n    # External API\n    def default_framer(self):\n        \"\"\"Return a bitcoin framer.\"\"\"\n        return BitcoinFramer(bytes.fromhex('e3e1f3e8'), 128_000_000)\n\n    async def handle_message(self, message):\n        \"\"\"message is a (command, payload) pair.\"\"\"\n        pass\n\n    async def send_message(self, message):\n        \"\"\"Send a message (command, payload) over the network.\"\"\"\n        await self._send_message(message)\n\n\nclass BatchError(Exception):\n\n    def __init__(self, request):\n        self.request = request   # BatchRequest object\n\n\nclass BatchRequest:\n    \"\"\"Used to build a batch request to send to the server.  Stores\n    the\n\n    Attributes batch and results are initially None.\n\n    Adding an invalid request or notification immediately raises a\n    ProtocolError.\n\n    On exiting the with clause, it will:\n\n    1) create a Batch object for the requests in the order they were\n       added.  If the batch is empty this raises a ProtocolError.\n\n    2) set the \"batch\" attribute to be that batch\n\n    3) send the batch request and wait for a response\n\n    4) raise a ProtocolError if the protocol was violated by the\n       server.  Currently this only happens if it gave more than one\n       response to any request\n\n    5) otherwise there is precisely one response to each Request.  Set\n       the \"results\" attribute to the tuple of results; the responses\n       are ordered to match the Requests in the batch.  Notifications\n       do not get a response.\n\n    6) if raise_errors is True and any individual response was a JSON\n       RPC error response, or violated the protocol in some way, a\n       BatchError exception is raised.  Otherwise the caller can be\n       certain each request returned a standard result.\n    \"\"\"\n\n    def __init__(self, session, raise_errors):\n        self._session = session\n        self._raise_errors = raise_errors\n        self._requests = []\n        self.batch = None\n        self.results = None\n\n    def add_request(self, method, args=()):\n        self._requests.append(Request(method, args))\n\n    def add_notification(self, method, args=()):\n        self._requests.append(Notification(method, args))\n\n    def __len__(self):\n        return len(self._requests)\n\n    async def __aenter__(self):\n        return self\n\n    async def __aexit__(self, exc_type, exc_value, traceback):\n        if exc_type is None:\n            self.batch = Batch(self._requests)\n            message, event = self._session.connection.send_batch(self.batch)\n            await self._session._send_message(message)\n            await event.wait()\n            self.results = event.result\n            if self._raise_errors:\n                if any(isinstance(item, Exception) for item in event.result):\n                    raise BatchError(self)\n\n\nNAMESPACE = \"wallet_server\"\n\n\nclass RPCSession(SessionBase):\n    \"\"\"Base class for protocols where a message can lead to a response,\n    for example JSON RPC.\"\"\"\n\n    RESPONSE_TIMES = Histogram(\"response_time\", \"Response times\", namespace=NAMESPACE,\n                               labelnames=(\"method\", \"version\"), buckets=HISTOGRAM_BUCKETS)\n    NOTIFICATION_COUNT = Counter(\"notification\", \"Number of notifications sent (for subscriptions)\",\n                                 namespace=NAMESPACE, labelnames=(\"method\", \"version\"))\n    REQUEST_ERRORS_COUNT = Counter(\n        \"request_error\", \"Number of requests that returned errors\", namespace=NAMESPACE,\n        labelnames=(\"method\", \"version\")\n    )\n    RESET_CONNECTIONS = Counter(\n        \"reset_clients\", \"Number of reset connections by client version\",\n        namespace=NAMESPACE, labelnames=(\"version\",)\n    )\n\n    def __init__(self, *, framer=None, connection=None):\n        super().__init__(framer=framer)\n        self.connection = connection or self.default_connection()\n        self.client_version = 'unknown'\n\n    async def _receive_messages(self):\n        while not self.is_closing():\n            try:\n                message = await self.framer.receive_message()\n            except MemoryError:\n                self.logger.warning('received oversized message from %s:%s, dropping connection',\n                                    self._address[0], self._address[1])\n                self.RESET_CONNECTIONS.labels(version=self.client_version).inc()\n                self._close()\n                return\n\n            self.last_recv = time.perf_counter()\n            self.recv_count += 1\n\n            try:\n                requests = self.connection.receive_message(message)\n            except ProtocolError as e:\n                self.logger.debug(f'{e}')\n                if e.error_message:\n                    await self._send_message(e.error_message)\n                if e.code == JSONRPC.PARSE_ERROR:\n                    self.max_errors = 0\n                self._bump_errors()\n            else:\n                for request in requests:\n                    await self._task_group.add(self._handle_request(request))\n\n    async def _handle_request(self, request):\n        start = time.perf_counter()\n        try:\n            result = await self.handle_request(request)\n        except (ProtocolError, RPCError) as e:\n            result = e\n        except CancelledError:\n            raise\n        except Exception:\n            reqstr = str(request)\n            self.logger.exception(f'exception handling {reqstr[:16_000]}')\n            result = RPCError(JSONRPC.INTERNAL_ERROR,\n                              'internal server error')\n        if isinstance(request, Request):\n            message = request.send_result(result)\n            self.RESPONSE_TIMES.labels(\n                method=request.method,\n                version=self.client_version\n            ).observe(time.perf_counter() - start)\n            if message:\n                await self._send_message(message)\n        if isinstance(result, Exception):\n            self._bump_errors()\n            self.REQUEST_ERRORS_COUNT.labels(\n                method=request.method,\n                version=self.client_version\n            ).inc()\n\n    def connection_lost(self, exc):\n        # Cancel pending requests and message processing\n        self.connection.raise_pending_requests(exc)\n        super().connection_lost(exc)\n\n    # External API\n    def default_connection(self):\n        \"\"\"Return a default connection if the user provides none.\"\"\"\n        return JSONRPCConnection(JSONRPCv2)\n\n    def default_framer(self):\n        \"\"\"Return a default framer.\"\"\"\n        return NewlineFramer()\n\n    async def handle_request(self, request):\n        pass\n\n    async def send_request(self, method, args=()):\n        \"\"\"Send an RPC request over the network.\"\"\"\n        if self.is_closing():\n            raise asyncio.TimeoutError(\"Trying to send request on a recently dropped connection.\")\n        message, event = self.connection.send_request(Request(method, args))\n        await self._send_message(message)\n        await event.wait()\n        result = event.result\n        if isinstance(result, Exception):\n            raise result\n        return result\n\n    async def send_notification(self, method, args=()) -> bool:\n        \"\"\"Send an RPC notification over the network.\"\"\"\n        message = self.connection.send_notification(Notification(method, args))\n        self.NOTIFICATION_COUNT.labels(method=method, version=self.client_version).inc()\n        try:\n            await self._send_message(message)\n            return True\n        except asyncio.TimeoutError:\n            self.logger.info(\"timeout sending address notification to %s\", self.peer_address_str(for_log=True))\n            self.abort()\n            return False\n\n    async def send_notifications(self, notifications) -> bool:\n        \"\"\"Send an RPC notification over the network.\"\"\"\n        message, _ = self.connection.send_batch(notifications)\n        try:\n            await self._send_message(message)\n            return True\n        except asyncio.TimeoutError:\n            self.logger.info(\"timeout sending address notification to %s\", self.peer_address_str(for_log=True))\n            self.abort()\n            return False\n\n    def send_batch(self, raise_errors=False):\n        \"\"\"Return a BatchRequest.  Intended to be used like so:\n\n           async with session.send_batch() as batch:\n               batch.add_request(\"method1\")\n               batch.add_request(\"sum\", (x, y))\n               batch.add_notification(\"updated\")\n\n           for result in batch.results:\n              ...\n\n        Note that in some circumstances exceptions can be raised; see\n        BatchRequest doc string.\n        \"\"\"\n        return BatchRequest(self, raise_errors)\n\n\nclass Server:\n    \"\"\"A simple wrapper around an asyncio.Server object.\"\"\"\n\n    def __init__(self, session_factory, host=None, port=None, *,\n                 loop=None, **kwargs):\n        self.host = host\n        self.port = port\n        self.loop = loop or asyncio.get_event_loop()\n        self.server = None\n        self._session_factory = session_factory\n        self._kwargs = kwargs\n\n    async def listen(self):\n        self.server = await self.loop.create_server(\n            self._session_factory, self.host, self.port, **self._kwargs)\n\n    async def close(self):\n        \"\"\"Close the listening socket.  This does not close any ServerSession\n        objects created to handle incoming connections.\n        \"\"\"\n        if self.server:\n            self.server.close()\n            await self.server.wait_closed()\n            self.server = None\n"
  },
  {
    "path": "lbry/wallet/rpc/socks.py",
    "content": "# Copyright (c) 2018, Neil Booth\n#\n# All rights reserved.\n#\n# The MIT License (MIT)\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n\"\"\"SOCKS proxying.\"\"\"\n\nimport sys\nimport asyncio\nimport collections\nimport ipaddress\nimport socket\nimport struct\nfrom functools import partial\n\n\n__all__ = ('SOCKSUserAuth', 'SOCKS4', 'SOCKS4a', 'SOCKS5', 'SOCKSProxy',\n           'SOCKSError', 'SOCKSProtocolError', 'SOCKSFailure')\n\n\nSOCKSUserAuth = collections.namedtuple(\"SOCKSUserAuth\", \"username password\")\n\n\nclass SOCKSError(Exception):\n    \"\"\"Base class for SOCKS exceptions.  Each raised exception will be\n    an instance of a derived class.\"\"\"\n\n\nclass SOCKSProtocolError(SOCKSError):\n    \"\"\"Raised when the proxy does not follow the SOCKS protocol\"\"\"\n\n\nclass SOCKSFailure(SOCKSError):\n    \"\"\"Raised when the proxy refuses or fails to make a connection\"\"\"\n\n\nclass NeedData(Exception):\n    pass\n\n\nclass SOCKSBase:\n\n    @classmethod\n    def name(cls):\n        return cls.__name__\n\n    def __init__(self):\n        self._buffer = bytes()\n        self._state = self._start\n\n    def _read(self, size):\n        if len(self._buffer) < size:\n            raise NeedData(size - len(self._buffer))\n        result = self._buffer[:size]\n        self._buffer = self._buffer[size:]\n        return result\n\n    def receive_data(self, data):\n        self._buffer += data\n\n    def next_message(self):\n        return self._state()\n\n\nclass SOCKS4(SOCKSBase):\n    \"\"\"SOCKS4 protocol wrapper.\"\"\"\n\n    # See http://ftp.icm.edu.pl/packages/socks/socks4/SOCKS4.protocol\n    REPLY_CODES = {\n        90: 'request granted',\n        91: 'request rejected or failed',\n        92: ('request rejected because SOCKS server cannot connect '\n             'to identd on the client'),\n        93: ('request rejected because the client program and identd '\n             'report different user-ids')\n    }\n\n    def __init__(self, dst_host, dst_port, auth):\n        super().__init__()\n        self._dst_host = self._check_host(dst_host)\n        self._dst_port = dst_port\n        self._auth = auth\n\n    @classmethod\n    def _check_host(cls, host):\n        if not isinstance(host, ipaddress.IPv4Address):\n            try:\n                host = ipaddress.IPv4Address(host)\n            except ValueError:\n                raise SOCKSProtocolError(\n                    f'SOCKS4 requires an IPv4 address: {host}') from None\n        return host\n\n    def _start(self):\n        self._state = self._first_response\n\n        if isinstance(self._dst_host, ipaddress.IPv4Address):\n            # SOCKS4\n            dst_ip_packed = self._dst_host.packed\n            host_bytes = b''\n        else:\n            # SOCKS4a\n            dst_ip_packed = b'\\0\\0\\0\\1'\n            host_bytes = self._dst_host.encode() + b'\\0'\n\n        if isinstance(self._auth, SOCKSUserAuth):\n            user_id = self._auth.username.encode()\n        else:\n            user_id = b''\n\n        # Send TCP/IP stream CONNECT request\n        return b''.join([b'\\4\\1', struct.pack('>H', self._dst_port),\n                         dst_ip_packed, user_id, b'\\0', host_bytes])\n\n    def _first_response(self):\n        # Wait for 8-byte response\n        data = self._read(8)\n        if data[0] != 0:\n            raise SOCKSProtocolError(f'invalid {self.name()} proxy '\n                                     f'response: {data}')\n        reply_code = data[1]\n        if reply_code != 90:\n            msg = self.REPLY_CODES.get(\n                reply_code, f'unknown {self.name()} reply code {reply_code}')\n            raise SOCKSFailure(f'{self.name()} proxy request failed: {msg}')\n\n        # Other fields ignored\n        return None\n\n\nclass SOCKS4a(SOCKS4):\n\n    @classmethod\n    def _check_host(cls, host):\n        if not isinstance(host, (str, ipaddress.IPv4Address)):\n            raise SOCKSProtocolError(\n                f'SOCKS4a requires an IPv4 address or host name: {host}')\n        return host\n\n\nclass SOCKS5(SOCKSBase):\n    \"\"\"SOCKS protocol wrapper.\"\"\"\n\n    # See https://tools.ietf.org/html/rfc1928\n    ERROR_CODES = {\n        1: 'general SOCKS server failure',\n        2: 'connection not allowed by ruleset',\n        3: 'network unreachable',\n        4: 'host unreachable',\n        5: 'connection refused',\n        6: 'TTL expired',\n        7: 'command not supported',\n        8: 'address type not supported',\n    }\n\n    def __init__(self, dst_host, dst_port, auth):\n        super().__init__()\n        self._dst_bytes = self._destination_bytes(dst_host, dst_port)\n        self._auth_bytes, self._auth_methods = self._authentication(auth)\n\n    def _destination_bytes(self, host, port):\n        if isinstance(host, ipaddress.IPv4Address):\n            addr_bytes = b'\\1' + host.packed\n        elif isinstance(host, ipaddress.IPv6Address):\n            addr_bytes = b'\\4' + host.packed\n        elif isinstance(host, str):\n            host = host.encode()\n            if len(host) > 255:\n                raise SOCKSProtocolError(f'hostname too long: '\n                                         f'{len(host)} bytes')\n            addr_bytes = b'\\3' + bytes([len(host)]) + host\n        else:\n            raise SOCKSProtocolError(f'SOCKS5 requires an IPv4 address, IPv6 '\n                                     f'address, or host name: {host}')\n        return addr_bytes + struct.pack('>H', port)\n\n    def _authentication(self, auth):\n        if isinstance(auth, SOCKSUserAuth):\n            user_bytes = auth.username.encode()\n            if not 0 < len(user_bytes) < 256:\n                raise SOCKSProtocolError(f'username {auth.username} has '\n                                         f'invalid length {len(user_bytes)}')\n            pwd_bytes = auth.password.encode()\n            if not 0 < len(pwd_bytes) < 256:\n                raise SOCKSProtocolError(f'password has invalid length '\n                                         f'{len(pwd_bytes)}')\n            return b''.join([bytes([1, len(user_bytes)]), user_bytes,\n                            bytes([len(pwd_bytes)]), pwd_bytes]), [0, 2]\n        return b'', [0]\n\n    def _start(self):\n        self._state = self._first_response\n        return (b'\\5' + bytes([len(self._auth_methods)])\n                + bytes(m for m in self._auth_methods))\n\n    def _first_response(self):\n        # Wait for 2-byte response\n        data = self._read(2)\n        if data[0] != 5:\n            raise SOCKSProtocolError(f'invalid SOCKS5 proxy response: {data}')\n        if data[1] not in self._auth_methods:\n            raise SOCKSFailure('SOCKS5 proxy rejected authentication methods')\n\n        # Authenticate if user-password authentication\n        if data[1] == 2:\n            self._state = self._auth_response\n            return self._auth_bytes\n        return self._request_connection()\n\n    def _auth_response(self):\n        data = self._read(2)\n        if data[0] != 1:\n            raise SOCKSProtocolError(f'invalid SOCKS5 proxy auth '\n                                     f'response: {data}')\n        if data[1] != 0:\n            raise SOCKSFailure(f'SOCKS5 proxy auth failure code: '\n                               f'{data[1]}')\n\n        return self._request_connection()\n\n    def _request_connection(self):\n        # Send connection request\n        self._state = self._connect_response\n        return b'\\5\\1\\0' + self._dst_bytes\n\n    def _connect_response(self):\n        data = self._read(5)\n        if data[0] != 5 or data[2] != 0 or data[3] not in (1, 3, 4):\n            raise SOCKSProtocolError(f'invalid SOCKS5 proxy response: {data}')\n        if data[1] != 0:\n            raise SOCKSFailure(self.ERROR_CODES.get(\n                data[1], f'unknown SOCKS5 error code: {data[1]}'))\n\n        if data[3] == 1:\n            addr_len = 3   # IPv4\n        elif data[3] == 3:\n            addr_len = data[4]  # Hostname\n        else:\n            addr_len = 15  # IPv6\n\n        self._state = partial(self._connect_response_rest, addr_len)\n        return self.next_message()\n\n    def _connect_response_rest(self, addr_len):\n        self._read(addr_len + 2)\n        return None\n\n\nclass SOCKSProxy:\n\n    def __init__(self, address, protocol, auth):\n        \"\"\"A SOCKS proxy at an address following a SOCKS protocol.  auth is an\n        authentication method to use when connecting, or None.\n\n        address is a (host, port) pair; for IPv6 it can instead be a\n        (host, port, flowinfo, scopeid) 4-tuple.\n        \"\"\"\n        self.address = address\n        self.protocol = protocol\n        self.auth = auth\n        # Set on each successful connection via the proxy to the\n        # result of socket.getpeername()\n        self.peername = None\n\n    def __str__(self):\n        auth = 'username' if self.auth else 'none'\n        return f'{self.protocol.name()} proxy at {self.address}, auth: {auth}'\n\n    async def _handshake(self, client, sock, loop):\n        while True:\n            count = 0\n            try:\n                message = client.next_message()\n            except NeedData as e:\n                count = e.args[0]\n            else:\n                if message is None:\n                    return\n                await loop.sock_sendall(sock, message)\n\n            if count:\n                data = await loop.sock_recv(sock, count)\n                if not data:\n                    raise SOCKSProtocolError(\"EOF received\")\n                client.receive_data(data)\n\n    async def _connect_one(self, host, port):\n        \"\"\"Connect to the proxy and perform a handshake requesting a\n        connection to (host, port).\n\n        Return the open socket on success, or the exception on failure.\n        \"\"\"\n        client = self.protocol(host, port, self.auth)\n        sock = socket.socket()\n        loop = asyncio.get_event_loop()\n        try:\n            # A non-blocking socket is required by loop socket methods\n            sock.setblocking(False)\n            await loop.sock_connect(sock, self.address)\n            await self._handshake(client, sock, loop)\n            self.peername = sock.getpeername()\n            return sock\n        except Exception as e:\n            # Don't close - see https://github.com/kyuupichan/aiorpcX/issues/8\n            if sys.platform.startswith('linux') or sys.platform == \"darwin\":\n                sock.close()\n            return e\n\n    async def _connect(self, addresses):\n        \"\"\"Connect to the proxy and perform a handshake requesting a\n        connection to each address in addresses.\n\n        Return an (open_socket, address) pair on success.\n        \"\"\"\n        assert len(addresses) > 0\n\n        exceptions = []\n        for address in addresses:\n            host, port = address[:2]\n            sock = await self._connect_one(host, port)\n            if isinstance(sock, socket.socket):\n                return sock, address\n            exceptions.append(sock)\n\n        strings = {f'{exc!r}' for exc in exceptions}\n        raise (exceptions[0] if len(strings) == 1 else\n               OSError(f'multiple exceptions: {\", \".join(strings)}'))\n\n    async def _detect_proxy(self):\n        \"\"\"Return True if it appears we can connect to a SOCKS proxy,\n        otherwise False.\n        \"\"\"\n        if self.protocol is SOCKS4a:\n            host, port = 'www.apple.com', 80\n        else:\n            host, port = ipaddress.IPv4Address('8.8.8.8'), 53\n\n        sock = await self._connect_one(host, port)\n        if isinstance(sock, socket.socket):\n            sock.close()\n            return True\n\n        # SOCKSFailure indicates something failed, but that we are\n        # likely talking to a proxy\n        return isinstance(sock, SOCKSFailure)\n\n    @classmethod\n    async def auto_detect_address(cls, address, auth):\n        \"\"\"Try to detect a SOCKS proxy at address using the authentication\n        method (or None).  SOCKS5, SOCKS4a and SOCKS are tried in\n        order.  If a SOCKS proxy is detected a SOCKSProxy object is\n        returned.\n\n        Returning a SOCKSProxy does not mean it is functioning - for\n        example, it may have no network connectivity.\n\n        If no proxy is detected return None.\n        \"\"\"\n        for protocol in (SOCKS5, SOCKS4a, SOCKS4):\n            proxy = cls(address, protocol, auth)\n            if await proxy._detect_proxy():\n                return proxy\n        return None\n\n    @classmethod\n    async def auto_detect_host(cls, host, ports, auth):\n        \"\"\"Try to detect a SOCKS proxy on a host on one of the ports.\n\n        Calls auto_detect for the ports in order.  Returns SOCKS are\n        tried in order; a SOCKSProxy object for the first detected\n        proxy is returned.\n\n        Returning a SOCKSProxy does not mean it is functioning - for\n        example, it may have no network connectivity.\n\n        If no proxy is detected return None.\n        \"\"\"\n        for port in ports:\n            address = (host, port)\n            proxy = await cls.auto_detect_address(address, auth)\n            if proxy:\n                return proxy\n\n        return None\n\n    async def create_connection(self, protocol_factory, host, port, *,\n                                resolve=False, ssl=None,\n                                family=0, proto=0, flags=0):\n        \"\"\"Set up a connection to (host, port) through the proxy.\n\n        If resolve is True then host is resolved locally with\n        getaddrinfo using family, proto and flags, otherwise the proxy\n        is asked to resolve host.\n\n        The function signature is similar to loop.create_connection()\n        with the same result.  The attribute _address is set on the\n        protocol to the address of the successful remote connection.\n        Additionally raises SOCKSError if something goes wrong with\n        the proxy handshake.\n        \"\"\"\n        loop = asyncio.get_event_loop()\n        if resolve:\n            infos = await loop.getaddrinfo(host, port, family=family,\n                                           type=socket.SOCK_STREAM,\n                                           proto=proto, flags=flags)\n            addresses = [info[4] for info in infos]\n        else:\n            addresses = [(host, port)]\n\n        sock, address = await self._connect(addresses)\n\n        def set_address():\n            protocol = protocol_factory()\n            protocol._address = address\n            return protocol\n\n        return await loop.create_connection(\n            set_address, sock=sock, ssl=ssl,\n            server_hostname=host if ssl else None)\n"
  },
  {
    "path": "lbry/wallet/rpc/util.py",
    "content": "# Copyright (c) 2018, Neil Booth\n#\n# All rights reserved.\n#\n# The MIT License (MIT)\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n__all__ = ()\n\n\nimport asyncio\nfrom collections import namedtuple\nimport inspect\n\n# other_params: None means cannot be called with keyword arguments only\n# any means any name is good\nfrom functools import lru_cache\n\nSignatureInfo = namedtuple('SignatureInfo', 'min_args max_args '\n                           'required_names other_names')\n\n\n@lru_cache(256)\ndef signature_info(func):\n    params = inspect.signature(func).parameters\n    min_args = max_args = 0\n    required_names = []\n    other_names = []\n    no_names = False\n    for p in params.values():\n        if p.kind == p.POSITIONAL_OR_KEYWORD:\n            max_args += 1\n            if p.default is p.empty:\n                min_args += 1\n                required_names.append(p.name)\n            else:\n                other_names.append(p.name)\n        elif p.kind == p.KEYWORD_ONLY:\n            other_names.append(p.name)\n        elif p.kind == p.VAR_POSITIONAL:\n            max_args = None\n        elif p.kind == p.VAR_KEYWORD:\n            other_names = any\n        elif p.kind == p.POSITIONAL_ONLY:\n            max_args += 1\n            if p.default is p.empty:\n                min_args += 1\n            no_names = True\n\n    if no_names:\n        other_names = None\n\n    return SignatureInfo(min_args, max_args, required_names, other_names)\n\n\nclass Concurrency:\n\n    def __init__(self, max_concurrent):\n        self._require_non_negative(max_concurrent)\n        self._max_concurrent = max_concurrent\n        self.semaphore = asyncio.Semaphore(max_concurrent)\n\n    def _require_non_negative(self, value):\n        if not isinstance(value, int) or value < 0:\n            raise RuntimeError('concurrency must be a natural number')\n\n    @property\n    def max_concurrent(self):\n        return self._max_concurrent\n\n    async def set_max_concurrent(self, value):\n        self._require_non_negative(value)\n        diff = value - self._max_concurrent\n        self._max_concurrent = value\n        if diff >= 0:\n            for _ in range(diff):\n                self.semaphore.release()\n        else:\n            for _ in range(-diff):\n                await self.semaphore.acquire()\n"
  },
  {
    "path": "lbry/wallet/script.py",
    "content": "from typing import List\nfrom itertools import chain\nfrom binascii import hexlify\nfrom collections import namedtuple\n\nfrom .bcd_data_stream import BCDataStream\nfrom .util import subclass_tuple\n\n\n# bitcoin opcodes\nOP_0 = 0x00\nOP_1 = 0x51\nOP_16 = 0x60\nOP_VERIFY = 0x69\nOP_DUP = 0x76\nOP_HASH160 = 0xa9\nOP_EQUALVERIFY = 0x88\nOP_CHECKSIG = 0xac\nOP_CHECKMULTISIG = 0xae\nOP_CHECKLOCKTIMEVERIFY = 0xb1\nOP_EQUAL = 0x87\nOP_PUSHDATA1 = 0x4c\nOP_PUSHDATA2 = 0x4d\nOP_PUSHDATA4 = 0x4e\nOP_RETURN = 0x6a\nOP_2DROP = 0x6d\nOP_DROP = 0x75\n\n# lbry custom opcodes\n# checks\nOP_PRICECHECK = 0xb0  # checks that the BUY output is >= SELL price\n# tx types\nOP_CLAIM_NAME = 0xb5\nOP_SUPPORT_CLAIM = 0xb6\nOP_UPDATE_CLAIM = 0xb7\nOP_SELL_CLAIM = 0xb8\nOP_BUY_CLAIM = 0xb9\n\n# template matching opcodes (not real opcodes)\n# base class for PUSH_DATA related opcodes\n# pylint: disable=invalid-name\nPUSH_DATA_OP = namedtuple('PUSH_DATA_OP', 'name')\n# opcode for variable length strings\n# pylint: disable=invalid-name\nPUSH_SINGLE = subclass_tuple('PUSH_SINGLE', PUSH_DATA_OP)\n# opcode for variable size integers\n# pylint: disable=invalid-name\nPUSH_INTEGER = subclass_tuple('PUSH_INTEGER', PUSH_DATA_OP)\n# opcode for variable number of variable length strings\n# pylint: disable=invalid-name\nPUSH_MANY = subclass_tuple('PUSH_MANY', PUSH_DATA_OP)\n# opcode with embedded subscript parsing\n# pylint: disable=invalid-name\nPUSH_SUBSCRIPT = namedtuple('PUSH_SUBSCRIPT', 'name template')\n\n\ndef is_push_data_opcode(opcode):\n    return isinstance(opcode, (PUSH_DATA_OP, PUSH_SUBSCRIPT))\n\n\ndef is_push_data_token(token):\n    return 1 <= token <= OP_PUSHDATA4\n\n\ndef push_data(data):\n    size = len(data)\n    if size < OP_PUSHDATA1:\n        yield BCDataStream.uint8.pack(size)\n    elif size <= 0xFF:\n        yield BCDataStream.uint8.pack(OP_PUSHDATA1)\n        yield BCDataStream.uint8.pack(size)\n    elif size <= 0xFFFF:\n        yield BCDataStream.uint8.pack(OP_PUSHDATA2)\n        yield BCDataStream.uint16.pack(size)\n    else:\n        yield BCDataStream.uint8.pack(OP_PUSHDATA4)\n        yield BCDataStream.uint32.pack(size)\n    yield bytes(data)\n\n\ndef read_data(token, stream):\n    if token < OP_PUSHDATA1:\n        return stream.read(token)\n    if token == OP_PUSHDATA1:\n        return stream.read(stream.read_uint8())\n    if token == OP_PUSHDATA2:\n        return stream.read(stream.read_uint16())\n    return stream.read(stream.read_uint32())\n\n\n# opcode for OP_1 - OP_16\n# pylint: disable=invalid-name\nSMALL_INTEGER = namedtuple('SMALL_INTEGER', 'name')\n\n\ndef is_small_integer(token):\n    return OP_1 <= token <= OP_16\n\n\ndef push_small_integer(num):\n    assert 1 <= num <= 16\n    yield BCDataStream.uint8.pack(OP_1 + (num - 1))\n\n\ndef read_small_integer(token):\n    return (token - OP_1) + 1\n\n\nclass Token(namedtuple('Token', 'value')):\n    __slots__ = ()\n\n    def __repr__(self):\n        name = None\n        for var_name, var_value in globals().items():\n            if var_name.startswith('OP_') and var_value == self.value:\n                name = var_name\n                break\n        return name or self.value\n\n\nclass DataToken(Token):\n    __slots__ = ()\n\n    def __repr__(self):\n        return f'\"{hexlify(self.value)}\"'\n\n\nclass SmallIntegerToken(Token):\n    __slots__ = ()\n\n    def __repr__(self):\n        return f'SmallIntegerToken({self.value})'\n\n\ndef token_producer(source):\n    token = source.read_uint8()\n    while token is not None:\n        if is_push_data_token(token):\n            yield DataToken(read_data(token, source))\n        elif is_small_integer(token):\n            yield SmallIntegerToken(read_small_integer(token))\n        else:\n            yield Token(token)\n        token = source.read_uint8()\n\n\ndef tokenize(source):\n    return list(token_producer(source))\n\n\nclass ScriptError(Exception):\n    \"\"\" General script handling error. \"\"\"\n\n\nclass ParseError(ScriptError):\n    \"\"\" Script parsing error. \"\"\"\n\n\nclass Parser:\n\n    def __init__(self, opcodes, tokens):\n        self.opcodes = opcodes\n        self.tokens = tokens\n        self.values = {}\n        self.token_index = 0\n        self.opcode_index = 0\n\n    def parse(self):\n        while self.token_index < len(self.tokens) and self.opcode_index < len(self.opcodes):\n            token = self.tokens[self.token_index]\n            opcode = self.opcodes[self.opcode_index]\n            if token.value == 0 and isinstance(opcode, PUSH_SINGLE):\n                token = DataToken(b'')\n            if isinstance(token, DataToken):\n                if isinstance(opcode, (PUSH_SINGLE, PUSH_INTEGER, PUSH_SUBSCRIPT)):\n                    self.push_single(opcode, token.value)\n                elif isinstance(opcode, PUSH_MANY):\n                    self.consume_many_non_greedy()\n                else:\n                    raise ParseError(f\"DataToken found but opcode was '{opcode}'.\")\n            elif isinstance(token, SmallIntegerToken):\n                if isinstance(opcode, SMALL_INTEGER):\n                    self.values[opcode.name] = token.value\n                else:\n                    raise ParseError(f\"SmallIntegerToken found but opcode was '{opcode}'.\")\n            elif token.value == opcode:\n                pass\n            else:\n                raise ParseError(f\"Token is '{token.value}' and opcode is '{opcode}'.\")\n            self.token_index += 1\n            self.opcode_index += 1\n\n        if self.token_index < len(self.tokens):\n            raise ParseError(\"Parse completed without all tokens being consumed.\")\n\n        if self.opcode_index < len(self.opcodes):\n            raise ParseError(\"Parse completed without all opcodes being consumed.\")\n\n        return self\n\n    def consume_many_non_greedy(self):\n        \"\"\" Allows PUSH_MANY to consume data without being greedy\n            in cases when one or more PUSH_SINGLEs follow a PUSH_MANY. This will\n            prioritize giving all PUSH_SINGLEs some data and only after that\n            subsume the rest into PUSH_MANY.\n        \"\"\"\n\n        token_values = []\n        while self.token_index < len(self.tokens):\n            token = self.tokens[self.token_index]\n            if not isinstance(token, DataToken):\n                self.token_index -= 1\n                break\n            token_values.append(token.value)\n            self.token_index += 1\n\n        push_opcodes = []\n        push_many_count = 0\n        while self.opcode_index < len(self.opcodes):\n            opcode = self.opcodes[self.opcode_index]\n            if not is_push_data_opcode(opcode):\n                self.opcode_index -= 1\n                break\n            if isinstance(opcode, PUSH_MANY):\n                push_many_count += 1\n            push_opcodes.append(opcode)\n            self.opcode_index += 1\n\n        if push_many_count > 1:\n            raise ParseError(\n                \"Cannot have more than one consecutive PUSH_MANY, as there is no way to tell which\"\n                \" token value should go into which PUSH_MANY.\"\n            )\n\n        if len(push_opcodes) > len(token_values):\n            raise ParseError(\n                \"Not enough token values to match all of the PUSH_MANY and PUSH_SINGLE opcodes.\"\n            )\n\n        many_opcode = push_opcodes.pop(0)\n\n        # consume data into PUSH_SINGLE opcodes, working backwards\n        for opcode in reversed(push_opcodes):\n            self.push_single(opcode, token_values.pop())\n\n        # finally PUSH_MANY gets everything that's left\n        self.values[many_opcode.name] = token_values\n\n    def push_single(self, opcode, value):\n        if isinstance(opcode, PUSH_SINGLE):\n            self.values[opcode.name] = value\n        elif isinstance(opcode, PUSH_INTEGER):\n            self.values[opcode.name] = int.from_bytes(value, 'little')\n        elif isinstance(opcode, PUSH_SUBSCRIPT):\n            self.values[opcode.name] = Script.from_source_with_template(value, opcode.template)\n        else:\n            raise ParseError(f\"Not a push single or subscript: {opcode}\")\n\n\nclass Template:\n\n    __slots__ = 'name', 'opcodes'\n\n    def __init__(self, name, opcodes):\n        self.name = name\n        self.opcodes = opcodes\n\n    def parse(self, tokens):\n        return Parser(self.opcodes, tokens).parse().values if self.opcodes else {}\n\n    def generate(self, values):\n        source = BCDataStream()\n        for opcode in self.opcodes:\n            if isinstance(opcode, PUSH_SINGLE):\n                data = values[opcode.name]\n                source.write_many(push_data(data))\n            elif isinstance(opcode, PUSH_INTEGER):\n                data = values[opcode.name]\n                source.write_many(push_data(\n                    data.to_bytes((data.bit_length() + 8) // 8, byteorder='little', signed=True)\n                ))\n            elif isinstance(opcode, PUSH_SUBSCRIPT):\n                data = values[opcode.name]\n                source.write_many(push_data(data.source))\n            elif isinstance(opcode, PUSH_MANY):\n                for data in values[opcode.name]:\n                    source.write_many(push_data(data))\n            elif isinstance(opcode, SMALL_INTEGER):\n                data = values[opcode.name]\n                source.write_many(push_small_integer(data))\n            else:\n                source.write_uint8(opcode)\n        return source.get_bytes()\n\n\nclass Script:\n\n    __slots__ = 'source', '_template', '_values', '_template_hint'\n\n    templates: List[Template] = []\n\n    NO_SCRIPT = Template('no_script', None)  # special case\n\n    def __init__(self, source=None, template=None, values=None, template_hint=None):\n        self.source = source\n        self._template = template\n        self._values = values\n        self._template_hint = template_hint\n        if source is None and template and values:\n            self.generate()\n\n    @property\n    def template(self):\n        if self._template is None:\n            self.parse(self._template_hint)\n        return self._template\n\n    @property\n    def values(self):\n        if self._values is None:\n            self.parse(self._template_hint)\n        return self._values\n\n    @property\n    def tokens(self):\n        return tokenize(BCDataStream(self.source))\n\n    @classmethod\n    def from_source_with_template(cls, source, template):\n        return cls(source, template_hint=template)\n\n    def parse(self, template_hint=None):\n        tokens = self.tokens\n        if not tokens and not template_hint:\n            template_hint = self.NO_SCRIPT\n        for template in chain((template_hint,), self.templates):\n            if not template:\n                continue\n            try:\n                self._values = template.parse(tokens)\n                self._template = template\n                return\n            except ParseError:\n                continue\n        raise ValueError(f'No matching templates for source: {hexlify(self.source)}')\n\n    def generate(self):\n        self.source = self.template.generate(self._values)\n\n\nclass InputScript(Script):\n\n    __slots__ = ()\n\n    REDEEM_PUBKEY = Template('pubkey', (\n        PUSH_SINGLE('signature'),\n    ))\n    REDEEM_PUBKEY_HASH = Template('pubkey_hash', (\n        PUSH_SINGLE('signature'), PUSH_SINGLE('pubkey')\n    ))\n    MULTI_SIG_SCRIPT = Template('multi_sig', (\n        SMALL_INTEGER('signatures_count'), PUSH_MANY('pubkeys'), SMALL_INTEGER('pubkeys_count'),\n        OP_CHECKMULTISIG\n    ))\n    REDEEM_SCRIPT_HASH_MULTI_SIG = Template('script_hash+multi_sig', (\n        OP_0, PUSH_MANY('signatures'), PUSH_SUBSCRIPT('script', MULTI_SIG_SCRIPT)\n    ))\n    TIME_LOCK_SCRIPT = Template('timelock', (\n        PUSH_INTEGER('height'), OP_CHECKLOCKTIMEVERIFY, OP_DROP,\n        # rest is identical to OutputScript.PAY_PUBKEY_HASH:\n        OP_DUP, OP_HASH160, PUSH_SINGLE('pubkey_hash'), OP_EQUALVERIFY, OP_CHECKSIG\n    ))\n    REDEEM_SCRIPT_HASH_TIME_LOCK = Template('script_hash+timelock', (\n        PUSH_SINGLE('signature'), PUSH_SINGLE('pubkey'), PUSH_SUBSCRIPT('script', TIME_LOCK_SCRIPT)\n    ))\n\n    templates = [\n        REDEEM_PUBKEY,\n        REDEEM_PUBKEY_HASH,\n        REDEEM_SCRIPT_HASH_TIME_LOCK,\n        REDEEM_SCRIPT_HASH_MULTI_SIG,\n    ]\n\n    @classmethod\n    def redeem_pubkey_hash(cls, signature, pubkey):\n        return cls(template=cls.REDEEM_PUBKEY_HASH, values={\n            'signature': signature,\n            'pubkey': pubkey\n        })\n\n    @classmethod\n    def redeem_multi_sig_script_hash(cls, signatures, pubkeys):\n        return cls(template=cls.REDEEM_SCRIPT_HASH_MULTI_SIG, values={\n            'signatures': signatures,\n            'script': cls(template=cls.MULTI_SIG_SCRIPT, values={\n                'signatures_count': len(signatures),\n                'pubkeys': pubkeys,\n                'pubkeys_count': len(pubkeys)\n            })\n        })\n\n    @classmethod\n    def redeem_time_lock_script_hash(cls, signature, pubkey, height=None, pubkey_hash=None, script_source=None):\n        if height and pubkey_hash:\n            script = cls(template=cls.TIME_LOCK_SCRIPT, values={\n                'height': height,\n                'pubkey_hash': pubkey_hash\n            })\n        elif script_source:\n            script = cls(source=script_source, template=cls.TIME_LOCK_SCRIPT)\n            script.parse(script.template)\n        else:\n            raise ValueError(\"script_source or both height and pubkey_hash are required.\")\n        return cls(template=cls.REDEEM_SCRIPT_HASH_TIME_LOCK, values={\n            'signature': signature,\n            'pubkey': pubkey,\n            'script': script\n        })\n\n    @property\n    def is_script_hash(self):\n        return self.template.name.startswith('script_hash+')\n\n\nclass OutputScript(Script):\n\n    __slots__ = ()\n\n    # output / payment script templates (aka scriptPubKey)\n    PAY_PUBKEY_FULL = Template('pay_pubkey_full', (\n        PUSH_SINGLE('pubkey'), OP_CHECKSIG\n    ))\n    PAY_PUBKEY_HASH = Template('pay_pubkey_hash', (\n        OP_DUP, OP_HASH160, PUSH_SINGLE('pubkey_hash'), OP_EQUALVERIFY, OP_CHECKSIG\n    ))\n    PAY_SCRIPT_HASH = Template('pay_script_hash', (\n        OP_HASH160, PUSH_SINGLE('script_hash'), OP_EQUAL\n    ))\n    PAY_SEGWIT = Template('pay_script_hash+segwit', (\n        OP_0, PUSH_SINGLE('script_hash')\n    ))\n    RETURN_DATA = Template('return_data', (\n        OP_RETURN, PUSH_SINGLE('data')\n    ))\n\n    CLAIM_NAME_OPCODES = (\n        OP_CLAIM_NAME, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim'),\n        OP_2DROP, OP_DROP\n    )\n    CLAIM_NAME_PUBKEY = Template('claim_name+pay_pubkey_hash', (\n        CLAIM_NAME_OPCODES + PAY_PUBKEY_HASH.opcodes\n    ))\n    CLAIM_NAME_SCRIPT = Template('claim_name+pay_script_hash', (\n        CLAIM_NAME_OPCODES + PAY_SCRIPT_HASH.opcodes\n    ))\n\n    SUPPORT_CLAIM_OPCODES = (\n        OP_SUPPORT_CLAIM, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim_id'),\n        OP_2DROP, OP_DROP\n    )\n    SUPPORT_CLAIM_PUBKEY = Template('support_claim+pay_pubkey_hash', (\n        SUPPORT_CLAIM_OPCODES + PAY_PUBKEY_HASH.opcodes\n    ))\n    SUPPORT_CLAIM_SCRIPT = Template('support_claim+pay_script_hash', (\n        SUPPORT_CLAIM_OPCODES + PAY_SCRIPT_HASH.opcodes\n    ))\n\n    SUPPORT_CLAIM_DATA_OPCODES = (\n        OP_SUPPORT_CLAIM, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim_id'), PUSH_SINGLE('support'),\n        OP_2DROP, OP_2DROP\n    )\n    SUPPORT_CLAIM_DATA_PUBKEY = Template('support_claim+data+pay_pubkey_hash', (\n        SUPPORT_CLAIM_DATA_OPCODES + PAY_PUBKEY_HASH.opcodes\n    ))\n    SUPPORT_CLAIM_DATA_SCRIPT = Template('support_claim+data+pay_script_hash', (\n        SUPPORT_CLAIM_DATA_OPCODES + PAY_SCRIPT_HASH.opcodes\n    ))\n\n    UPDATE_CLAIM_OPCODES = (\n        OP_UPDATE_CLAIM, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim_id'), PUSH_SINGLE('claim'),\n        OP_2DROP, OP_2DROP\n    )\n    UPDATE_CLAIM_PUBKEY = Template('update_claim+pay_pubkey_hash', (\n        UPDATE_CLAIM_OPCODES + PAY_PUBKEY_HASH.opcodes\n    ))\n    UPDATE_CLAIM_SCRIPT = Template('update_claim+pay_script_hash', (\n        UPDATE_CLAIM_OPCODES + PAY_SCRIPT_HASH.opcodes\n    ))\n\n    templates = [\n        PAY_PUBKEY_FULL,\n        PAY_PUBKEY_HASH,\n        PAY_SCRIPT_HASH,\n        PAY_SEGWIT,\n        RETURN_DATA,\n        CLAIM_NAME_PUBKEY,\n        CLAIM_NAME_SCRIPT,\n        SUPPORT_CLAIM_PUBKEY,\n        SUPPORT_CLAIM_SCRIPT,\n        SUPPORT_CLAIM_DATA_PUBKEY,\n        SUPPORT_CLAIM_DATA_SCRIPT,\n        UPDATE_CLAIM_PUBKEY,\n        UPDATE_CLAIM_SCRIPT,\n    ]\n\n    @classmethod\n    def pay_pubkey_hash(cls, pubkey_hash):\n        return cls(template=cls.PAY_PUBKEY_HASH, values={\n            'pubkey_hash': pubkey_hash\n        })\n\n    @classmethod\n    def pay_script_hash(cls, script_hash):\n        return cls(template=cls.PAY_SCRIPT_HASH, values={\n            'script_hash': script_hash\n        })\n\n    @classmethod\n    def return_data(cls, data):\n        return cls(template=cls.RETURN_DATA, values={\n            'data': data\n        })\n\n    @property\n    def is_pay_pubkey(self):\n        return self.template.name.endswith('pay_pubkey_full')\n\n    @classmethod\n    def pay_claim_name_pubkey_hash(cls, claim_name, claim, pubkey_hash):\n        return cls(template=cls.CLAIM_NAME_PUBKEY, values={\n            'claim_name': claim_name,\n            'claim': claim,\n            'pubkey_hash': pubkey_hash\n        })\n\n    @classmethod\n    def pay_update_claim_pubkey_hash(cls, claim_name, claim_id, claim, pubkey_hash):\n        return cls(template=cls.UPDATE_CLAIM_PUBKEY, values={\n            'claim_name': claim_name,\n            'claim_id': claim_id,\n            'claim': claim,\n            'pubkey_hash': pubkey_hash\n        })\n\n    @classmethod\n    def pay_support_pubkey_hash(cls, claim_name: bytes, claim_id: bytes, pubkey_hash: bytes):\n        return cls(template=cls.SUPPORT_CLAIM_PUBKEY, values={\n            'claim_name': claim_name,\n            'claim_id': claim_id,\n            'pubkey_hash': pubkey_hash\n        })\n\n    @classmethod\n    def pay_support_data_pubkey_hash(\n            cls, claim_name: bytes, claim_id: bytes, support, pubkey_hash: bytes):\n        return cls(template=cls.SUPPORT_CLAIM_DATA_PUBKEY, values={\n            'claim_name': claim_name,\n            'claim_id': claim_id,\n            'support': support,\n            'pubkey_hash': pubkey_hash\n        })\n\n    @property\n    def is_pay_pubkey_hash(self):\n        return self.template.name.endswith('pay_pubkey_hash')\n\n    @property\n    def is_pay_script_hash(self):\n        return self.template.name.endswith('pay_script_hash')\n\n    @property\n    def is_return_data(self):\n        return self.template.name.endswith('return_data')\n\n    @property\n    def is_claim_name(self):\n        return self.template.name.startswith('claim_name+')\n\n    @property\n    def is_update_claim(self):\n        return self.template.name.startswith('update_claim+')\n\n    @property\n    def is_support_claim(self):\n        return self.template.name.startswith('support_claim+')\n\n    @property\n    def is_support_claim_data(self):\n        return self.template.name.startswith('support_claim+data+')\n\n    @property\n    def is_claim_involved(self):\n        return any((self.is_claim_name, self.is_support_claim, self.is_update_claim))\n"
  },
  {
    "path": "lbry/wallet/stream.py",
    "content": "import asyncio\n\n\nclass BroadcastSubscription:\n\n    def __init__(self, controller, on_data, on_error, on_done):\n        self._controller = controller\n        self._previous = self._next = None\n        self._on_data = on_data\n        self._on_error = on_error\n        self._on_done = on_done\n        self.is_paused = False\n        self.is_canceled = False\n        self.is_closed = False\n\n    def pause(self):\n        self.is_paused = True\n\n    def resume(self):\n        self.is_paused = False\n\n    def cancel(self):\n        self._controller._cancel(self)\n        self.is_canceled = True\n\n    @property\n    def can_fire(self):\n        return not any((self.is_paused, self.is_canceled, self.is_closed))\n\n    def _add(self, data):\n        if self.can_fire and self._on_data is not None:\n            return self._on_data(data)\n\n    def _add_error(self, exception):\n        if self.can_fire and self._on_error is not None:\n            return self._on_error(exception)\n\n    def _close(self):\n        try:\n            if self.can_fire and self._on_done is not None:\n                return self._on_done()\n        finally:\n            self.is_closed = True\n\n\nclass StreamController:\n\n    def __init__(self, merge_repeated_events=False):\n        self.stream = Stream(self)\n        self._first_subscription = None\n        self._last_subscription = None\n        self._last_event = None\n        self._merge_repeated = merge_repeated_events\n\n    @property\n    def has_listener(self):\n        return self._first_subscription is not None\n\n    @property\n    def _iterate_subscriptions(self):\n        next_sub = self._first_subscription\n        while next_sub is not None:\n            subscription = next_sub\n            next_sub = next_sub._next\n            yield subscription\n\n    def _notify_and_ensure_future(self, notify):\n        tasks = []\n        for subscription in self._iterate_subscriptions:\n            maybe_coroutine = notify(subscription)\n            if asyncio.iscoroutine(maybe_coroutine):\n                tasks.append(maybe_coroutine)\n        if tasks:\n            return asyncio.ensure_future(asyncio.wait(tasks))\n        else:\n            f = asyncio.get_event_loop().create_future()\n            f.set_result(None)\n            return f\n\n    def add(self, event):\n        skip = self._merge_repeated and event == self._last_event\n        self._last_event = event\n        return self._notify_and_ensure_future(\n            lambda subscription: None if skip else subscription._add(event)\n        )\n\n    def add_error(self, exception):\n        return self._notify_and_ensure_future(\n            lambda subscription: subscription._add_error(exception)\n        )\n\n    def close(self):\n        for subscription in self._iterate_subscriptions:\n            subscription._close()\n\n    def _cancel(self, subscription):\n        previous = subscription._previous\n        next_sub = subscription._next\n        if previous is None:\n            self._first_subscription = next_sub\n        else:\n            previous._next = next_sub\n        if next_sub is None:\n            self._last_subscription = previous\n        else:\n            next_sub._previous = previous\n        subscription._next = subscription._previous = subscription\n\n    def _listen(self, on_data, on_error, on_done):\n        subscription = BroadcastSubscription(self, on_data, on_error, on_done)\n        old_last = self._last_subscription\n        self._last_subscription = subscription\n        subscription._previous = old_last\n        subscription._next = None\n        if old_last is None:\n            self._first_subscription = subscription\n        else:\n            old_last._next = subscription\n        return subscription\n\n\nclass Stream:\n\n    def __init__(self, controller):\n        self._controller = controller\n\n    def listen(self, on_data, on_error=None, on_done=None):\n        return self._controller._listen(on_data, on_error, on_done)\n\n    def where(self, condition) -> asyncio.Future:\n        future = asyncio.get_event_loop().create_future()\n\n        def where_test(value):\n            if condition(value):\n                self._cancel_and_callback(subscription, future, value)\n\n        subscription = self.listen(\n            where_test,\n            lambda exception: self._cancel_and_error(subscription, future, exception)\n        )\n\n        return future\n\n    @property\n    def first(self):\n        future = asyncio.get_event_loop().create_future()\n        subscription = self.listen(\n            lambda value: not future.done() and self._cancel_and_callback(subscription, future, value),\n            lambda exception: not future.done() and self._cancel_and_error(subscription, future, exception)\n        )\n        return future\n\n    @staticmethod\n    def _cancel_and_callback(subscription: BroadcastSubscription, future: asyncio.Future, value):\n        subscription.cancel()\n        future.set_result(value)\n\n    @staticmethod\n    def _cancel_and_error(subscription: BroadcastSubscription, future: asyncio.Future, exception):\n        subscription.cancel()\n        future.set_exception(exception)\n"
  },
  {
    "path": "lbry/wallet/tasks.py",
    "content": "from asyncio import Event, get_event_loop\n\n\nclass TaskGroup:\n\n    def __init__(self, loop=None):\n        self._loop = loop or get_event_loop()\n        self._tasks = set()\n        self.done = Event()\n        self.started = Event()\n\n    def __len__(self):\n        return len(self._tasks)\n\n    def add(self, coro):\n        task = self._loop.create_task(coro)\n        self._tasks.add(task)\n        self.started.set()\n        self.done.clear()\n        task.add_done_callback(self._remove)\n        return task\n\n    def _remove(self, task):\n        self._tasks.remove(task)\n        if len(self._tasks) < 1:\n            self.done.set()\n            self.started.clear()\n\n    def cancel(self):\n        for task in self._tasks:\n            task.cancel()\n        self.done.set()\n        self.started.clear()\n"
  },
  {
    "path": "lbry/wallet/transaction.py",
    "content": "import struct\nimport logging\nimport typing\nfrom binascii import hexlify, unhexlify\nfrom typing import List, Iterable, Optional, Tuple\n\nfrom lbry.error import InsufficientFundsError\nfrom lbry.crypto.hash import hash160, sha256\nfrom lbry.crypto.base58 import Base58\nfrom lbry.schema.url import normalize_name\nfrom lbry.schema.claim import Claim\nfrom lbry.schema.base import Signable\nfrom lbry.schema.purchase import Purchase\nfrom lbry.schema.support import Support\n\nfrom .script import InputScript, OutputScript\nfrom .constants import COIN, DUST, NULL_HASH32\nfrom .bcd_data_stream import BCDataStream\nfrom .hash import TXRef, TXRefImmutable\nfrom .util import ReadOnlyList\nfrom .bip32 import PrivateKey, PublicKey\n\nif typing.TYPE_CHECKING:\n    from lbry.wallet.account import Account\n    from lbry.wallet.ledger import Ledger\n    from lbry.wallet.wallet import Wallet\n\nlog = logging.getLogger()\n\n\nclass TXRefMutable(TXRef):\n\n    __slots__ = ('tx',)\n\n    def __init__(self, tx: 'Transaction') -> None:\n        super().__init__()\n        self.tx = tx\n\n    @property\n    def id(self):\n        if self._id is None:\n            self._id = hexlify(self.hash[::-1]).decode()\n        return self._id\n\n    @property\n    def hash(self):\n        if self._hash is None:\n            self._hash = sha256(sha256(self.tx.raw_sans_segwit))\n        return self._hash\n\n    @property\n    def height(self):\n        return self.tx.height\n\n    def reset(self):\n        self._id = None\n        self._hash = None\n\n\nclass TXORef:\n\n    __slots__ = 'tx_ref', 'position'\n\n    def __init__(self, tx_ref: TXRef, position: int) -> None:\n        self.tx_ref = tx_ref\n        self.position = position\n\n    @property\n    def id(self):\n        return f'{self.tx_ref.id}:{self.position}'\n\n    @property\n    def hash(self):\n        return self.tx_ref.hash + BCDataStream.uint32.pack(self.position)\n\n    @property\n    def is_null(self):\n        return self.tx_ref.is_null\n\n    @property\n    def txo(self) -> Optional['Output']:\n        return None\n\n\nclass TXORefResolvable(TXORef):\n\n    __slots__ = ('_txo',)\n\n    def __init__(self, txo: 'Output') -> None:\n        assert txo.tx_ref is not None\n        assert txo.position is not None\n        super().__init__(txo.tx_ref, txo.position)\n        self._txo = txo\n\n    @property\n    def txo(self):\n        return self._txo\n\n\nclass InputOutput:\n\n    __slots__ = 'tx_ref', 'position'\n\n    def __init__(self, tx_ref: TXRef = None, position: int = None) -> None:\n        self.tx_ref = tx_ref\n        self.position = position\n\n    @property\n    def size(self) -> int:\n        \"\"\" Size of this input / output in bytes. \"\"\"\n        stream = BCDataStream()\n        self.serialize_to(stream)\n        return len(stream.get_bytes())\n\n    def get_fee(self, ledger):\n        return self.size * ledger.fee_per_byte\n\n    def serialize_to(self, stream, alternate_script=None):\n        raise NotImplementedError\n\n\nclass Input(InputOutput):\n\n    NULL_SIGNATURE = b'\\x00'*72\n    NULL_PUBLIC_KEY = b'\\x00'*33\n\n    __slots__ = 'txo_ref', 'sequence', 'coinbase', 'script'\n\n    def __init__(self, txo_ref: TXORef, script: InputScript, sequence: int = 0xFFFFFFFF,\n                 tx_ref: TXRef = None, position: int = None) -> None:\n        super().__init__(tx_ref, position)\n        self.txo_ref = txo_ref\n        self.sequence = sequence\n        self.coinbase = script if txo_ref.is_null else None\n        self.script = script if not txo_ref.is_null else None\n\n    @property\n    def is_coinbase(self):\n        return self.coinbase is not None\n\n    @classmethod\n    def spend(cls, txo: 'Output') -> 'Input':\n        \"\"\" Create an input to spend the output.\"\"\"\n        assert txo.script.is_pay_pubkey_hash, 'Attempting to spend unsupported output.'\n        script = InputScript.redeem_pubkey_hash(cls.NULL_SIGNATURE, cls.NULL_PUBLIC_KEY)\n        return cls(txo.ref, script)\n\n    @classmethod\n    def spend_time_lock(cls, txo: 'Output', script_source: bytes) -> 'Input':\n        \"\"\" Create an input to spend time lock script.\"\"\"\n        script = InputScript.redeem_time_lock_script_hash(\n            cls.NULL_SIGNATURE, cls.NULL_PUBLIC_KEY, script_source=script_source\n        )\n        return cls(txo.ref, script)\n\n    @property\n    def amount(self) -> int:\n        \"\"\" Amount this input adds to the transaction. \"\"\"\n        if self.txo_ref.txo is None:\n            raise ValueError('Cannot resolve output to get amount.')\n        return self.txo_ref.txo.amount\n\n    @property\n    def is_my_input(self) -> Optional[bool]:\n        \"\"\" True if the output this input spends is yours. \"\"\"\n        if self.txo_ref.txo is None:\n            return False\n        return self.txo_ref.txo.is_my_output\n\n    @classmethod\n    def deserialize_from(cls, stream):\n        tx_ref = TXRefImmutable.from_hash(stream.read(32), -1)\n        position = stream.read_uint32()\n        script = stream.read_string()\n        sequence = stream.read_uint32()\n        return cls(\n            TXORef(tx_ref, position),\n            InputScript(script) if not tx_ref.is_null else script,\n            sequence\n        )\n\n    def serialize_to(self, stream, alternate_script=None):\n        stream.write(self.txo_ref.tx_ref.hash)\n        stream.write_uint32(self.txo_ref.position)\n        if alternate_script is not None:\n            stream.write_string(alternate_script)\n        else:\n            if self.is_coinbase:\n                stream.write_string(self.coinbase)\n            else:\n                stream.write_string(self.script.source)\n        stream.write_uint32(self.sequence)\n\n\nclass OutputEffectiveAmountEstimator:\n\n    __slots__ = 'txo', 'txi', 'fee', 'effective_amount'\n\n    def __init__(self, ledger: 'Ledger', txo: 'Output') -> None:\n        self.txo = txo\n        self.txi = Input.spend(txo)\n        self.fee: int = self.txi.get_fee(ledger)\n        self.effective_amount: int = txo.amount - self.fee\n\n    def __lt__(self, other):\n        return self.effective_amount < other.effective_amount\n\n\nclass Output(InputOutput):\n\n    __slots__ = (\n        'amount', 'script', 'is_internal_transfer', 'is_spent', 'is_my_output', 'is_my_input',\n        'channel', 'private_key', 'meta', 'sent_supports', 'sent_tips', 'received_tips',\n        'purchase', 'purchased_claim', 'purchase_receipt',\n        'reposted_claim', 'claims', '_signable'\n    )\n\n    def __init__(self, amount: int, script: OutputScript,\n                 tx_ref: TXRef = None, position: int = None,\n                 is_internal_transfer: Optional[bool] = None, is_spent: Optional[bool] = None,\n                 is_my_output: Optional[bool] = None, is_my_input: Optional[bool] = None,\n                 sent_supports: Optional[int] = None, sent_tips: Optional[int] = None,\n                 received_tips: Optional[int] = None,\n                 channel: Optional['Output'] = None,\n                 private_key: Optional[PrivateKey] = None\n                 ) -> None:\n        super().__init__(tx_ref, position)\n        self.amount = amount\n        self.script = script\n        self.is_internal_transfer = is_internal_transfer\n        self.is_spent = is_spent\n        self.is_my_output = is_my_output\n        self.is_my_input = is_my_input\n        self.sent_supports = sent_supports\n        self.sent_tips = sent_tips\n        self.received_tips = received_tips\n        self.channel = channel\n        self.private_key: PrivateKey = private_key\n        self.purchase: 'Output' = None  # txo containing purchase metadata\n        self.purchased_claim: 'Output' = None  # resolved claim pointed to by purchase\n        self.purchase_receipt: 'Output' = None  # txo representing purchase receipt for this claim\n        self.reposted_claim: 'Output' = None  # txo representing claim being reposted\n        self.claims: List['Output'] = None  # resolved claims for collection\n        self._signable: Optional[Signable] = None\n        self.meta = {}\n\n    def update_annotations(self, annotated: 'Output'):\n        if annotated is None:\n            self.is_internal_transfer = None\n            self.is_spent = None\n            self.is_my_output = None\n            self.is_my_input = None\n            self.sent_supports = None\n            self.sent_tips = None\n            self.received_tips = None\n        else:\n            self.is_internal_transfer = annotated.is_internal_transfer\n            self.is_spent = annotated.is_spent\n            self.is_my_output = annotated.is_my_output\n            self.is_my_input = annotated.is_my_input\n            self.sent_supports = annotated.sent_supports\n            self.sent_tips = annotated.sent_tips\n            self.received_tips = annotated.received_tips\n        self.channel = annotated.channel if annotated else None\n        self.private_key = annotated.private_key if annotated else None\n\n    @property\n    def ref(self):\n        return TXORefResolvable(self)\n\n    @property\n    def id(self):\n        return self.ref.id\n\n    @property\n    def is_pubkey_hash(self):\n        return 'pubkey_hash' in self.script.values\n\n    @property\n    def pubkey_hash(self):\n        return self.script.values['pubkey_hash']\n\n    @property\n    def is_script_hash(self):\n        return 'script_hash' in self.script.values\n\n    @property\n    def script_hash(self):\n        return self.script.values['script_hash']\n\n    @property\n    def has_address(self):\n        return self.is_pubkey_hash or self.is_script_hash\n\n    def get_address(self, ledger):\n        if self.is_pubkey_hash:\n            return ledger.hash160_to_address(self.pubkey_hash)\n        elif self.is_script_hash:\n            return ledger.hash160_to_script_address(self.script_hash)\n\n    def get_estimator(self, ledger):\n        return OutputEffectiveAmountEstimator(ledger, self)\n\n    @classmethod\n    def pay_pubkey_hash(cls, amount, pubkey_hash):\n        return cls(amount, OutputScript.pay_pubkey_hash(pubkey_hash))\n\n    @classmethod\n    def pay_script_hash(cls, amount, pubkey_hash):\n        return cls(amount, OutputScript.pay_script_hash(pubkey_hash))\n\n    @classmethod\n    def deserialize_from(cls, stream):\n        return cls(\n            amount=stream.read_uint64(),\n            script=OutputScript(stream.read_string())\n        )\n\n    def serialize_to(self, stream, alternate_script=None):\n        stream.write_uint64(self.amount)\n        stream.write_string(self.script.source)\n\n    def get_fee(self, ledger):\n        name_fee = 0\n        if self.script.is_claim_name:\n            name_fee = len(self.script.values['claim_name']) * ledger.fee_per_name_char\n        return max(name_fee, super().get_fee(ledger))\n\n    @property\n    def is_claim(self) -> bool:\n        return self.script.is_claim_name or self.script.is_update_claim\n\n    @property\n    def is_support(self) -> bool:\n        return self.script.is_support_claim\n\n    @property\n    def is_support_data(self) -> bool:\n        return self.script.is_support_claim_data\n\n    @property\n    def claim_hash(self) -> bytes:\n        if self.script.is_claim_name:\n            return hash160(self.tx_ref.hash + struct.pack('>I', self.position))\n        elif self.script.is_update_claim or self.script.is_support_claim:\n            return self.script.values['claim_id']\n        else:\n            raise ValueError('No claim_id associated.')\n\n    @property\n    def claim_id(self) -> str:\n        return hexlify(self.claim_hash[::-1]).decode()\n\n    @property\n    def claim_name(self) -> str:\n        if self.script.is_claim_involved:\n            return self.script.values['claim_name'].decode()\n        raise ValueError('No claim_name associated.')\n\n    @property\n    def normalized_name(self) -> str:\n        return normalize_name(self.claim_name)\n\n    @property\n    def claim(self) -> Claim:\n        if self.is_claim:\n            if not isinstance(self.script.values['claim'], Claim):\n                self.script.values['claim'] = Claim.from_bytes(self.script.values['claim'])\n            return self.script.values['claim']\n        raise ValueError('Only claim name and claim update have the claim payload.')\n\n    @property\n    def can_decode_claim(self):\n        try:\n            return self.claim\n        except Exception:\n            return False\n\n    @property\n    def support(self) -> Support:\n        if self.is_support_data:\n            if not isinstance(self.script.values['support'], Support):\n                self.script.values['support'] = Support.from_bytes(self.script.values['support'])\n            return self.script.values['support']\n        raise ValueError('Only supports with data can be represented as Supports.')\n\n    @property\n    def can_decode_support(self):\n        try:\n            return self.support\n        except Exception:\n            return False\n\n    @property\n    def signable(self) -> Signable:\n        if self._signable is None:\n            if self.is_claim:\n                self._signable = self.claim\n            elif self.is_support_data:\n                self._signable = self.support\n        return self._signable\n\n    @property\n    def permanent_url(self) -> str:\n        if self.script.is_claim_involved:\n            return f\"lbry://{self.claim_name}#{self.claim_id}\"\n        raise ValueError('No claim associated.')\n\n    @property\n    def has_private_key(self):\n        return self.private_key is not None\n\n    def get_signature_digest(self, ledger):\n        if self.signable.unsigned_payload:\n            pieces = [\n                Base58.decode(self.get_address(ledger)),\n                self.signable.unsigned_payload,\n                self.signable.signing_channel_hash[::-1]\n            ]\n        else:\n            pieces = [\n                self.tx_ref.tx.inputs[0].txo_ref.hash,\n                self.signable.signing_channel_hash,\n                self.signable.to_message_bytes()\n            ]\n        return sha256(b''.join(pieces))\n\n    @staticmethod\n    def is_signature_valid(signature, digest, public_key_bytes):\n        return PublicKey\\\n            .from_compressed(public_key_bytes)\\\n            .verify(signature, digest)\n\n    def is_signed_by(self, channel: 'Output', ledger=None):\n        return self.is_signature_valid(\n            self.signable.signature,\n            self.get_signature_digest(ledger),\n            channel.claim.channel.public_key_bytes\n        )\n\n    def sign(self, channel: 'Output', first_input_id=None):\n        self.channel = channel\n        self.signable.signing_channel_hash = channel.claim_hash\n        digest = sha256(b''.join([\n            first_input_id or self.tx_ref.tx.inputs[0].txo_ref.hash,\n            self.signable.signing_channel_hash,\n            self.signable.to_message_bytes()\n        ]))\n        self.signable.signature = channel.private_key.sign_compact(digest)\n        self.script.generate()\n\n    def sign_data(self, data: bytes, timestamp: str) -> str:\n        pieces = [timestamp.encode(), self.claim_hash, data]\n        digest = sha256(b''.join(pieces))\n        signature = self.private_key.sign_compact(digest)\n        return hexlify(signature).decode()\n\n    def clear_signature(self):\n        self.channel = None\n        self.signable.clear_signature()\n\n    def set_channel_private_key(self, private_key: PrivateKey):\n        self.private_key = private_key\n        self.claim.channel.public_key_bytes = private_key.public_key.pubkey_bytes\n        self.script.generate()\n        return self.private_key\n\n    def is_channel_private_key(self, private_key: PrivateKey):\n        return self.claim.channel.public_key_bytes == private_key.public_key.pubkey_bytes\n\n    @classmethod\n    def pay_claim_name_pubkey_hash(\n            cls, amount: int, claim_name: str, claim: Claim, pubkey_hash: bytes) -> 'Output':\n        script = OutputScript.pay_claim_name_pubkey_hash(\n            claim_name.encode(), claim, pubkey_hash)\n        return cls(amount, script)\n\n    @classmethod\n    def pay_update_claim_pubkey_hash(\n            cls, amount: int, claim_name: str, claim_id: str, claim: Claim, pubkey_hash: bytes) -> 'Output':\n        script = OutputScript.pay_update_claim_pubkey_hash(\n            claim_name.encode(), unhexlify(claim_id)[::-1], claim, pubkey_hash\n        )\n        return cls(amount, script)\n\n    @classmethod\n    def pay_support_pubkey_hash(cls, amount: int, claim_name: str, claim_id: str, pubkey_hash: bytes) -> 'Output':\n        script = OutputScript.pay_support_pubkey_hash(\n            claim_name.encode(), unhexlify(claim_id)[::-1], pubkey_hash\n        )\n        return cls(amount, script)\n\n    @classmethod\n    def pay_support_data_pubkey_hash(\n            cls, amount: int, claim_name: str, claim_id: str, support: Support, pubkey_hash: bytes) -> 'Output':\n        script = OutputScript.pay_support_data_pubkey_hash(\n            claim_name.encode(), unhexlify(claim_id)[::-1], support, pubkey_hash\n        )\n        return cls(amount, script)\n\n    @classmethod\n    def add_purchase_data(cls, purchase: Purchase) -> 'Output':\n        script = OutputScript.return_data(purchase)\n        return cls(0, script)\n\n    @property\n    def is_purchase_data(self) -> bool:\n        return self.script.is_return_data and (\n            isinstance(self.script.values['data'], Purchase) or\n            Purchase.has_start_byte(self.script.values['data'])\n        )\n\n    @property\n    def purchase_data(self) -> Purchase:\n        if self.is_purchase_data:\n            if not isinstance(self.script.values['data'], Purchase):\n                self.script.values['data'] = Purchase.from_bytes(self.script.values['data'])\n            return self.script.values['data']\n        raise ValueError('Output does not have purchase data.')\n\n    @property\n    def can_decode_purchase_data(self):\n        try:\n            return self.purchase_data\n        except:  # pylint: disable=bare-except\n            return False\n\n    @property\n    def purchased_claim_id(self):\n        if self.purchase is not None:\n            return self.purchase.purchase_data.claim_id\n        if self.purchased_claim is not None:\n            return self.purchased_claim.claim_id\n\n    @property\n    def has_price(self):\n        if self.can_decode_claim:\n            claim = self.claim\n            if claim.is_stream:\n                stream = claim.stream\n                return stream.has_fee and stream.fee.amount and stream.fee.amount > 0\n        return False\n\n    @property\n    def price(self):\n        return self.claim.stream.fee\n\n\nclass Transaction:\n\n    def __init__(self, raw=None, version: int = 1, locktime: int = 0, is_verified: bool = False,\n                 height: int = -2, position: int = -1, julian_day: int = None) -> None:\n        self._raw = raw\n        self._raw_sans_segwit = None\n        self._raw_outputs = None\n        self.is_segwit_flag = 0\n        self.witnesses: List[bytes] = []\n        self.ref = TXRefMutable(self)\n        self.version = version\n        self.locktime = locktime\n        self._inputs: List[Input] = []\n        self._outputs: List[Output] = []\n        self.is_verified = is_verified\n        # Height Progression\n        #   -2: not broadcast\n        #   -1: in mempool but has unconfirmed inputs\n        #    0: in mempool and all inputs confirmed\n        # +num: confirmed in a specific block (height)\n        self.height = height\n        self.position = position\n        self._day = julian_day\n        if raw is not None:\n            self._deserialize()\n\n    @property\n    def is_broadcast(self):\n        return self.height > -2\n\n    @property\n    def is_mempool(self):\n        return self.height in (-1, 0)\n\n    @property\n    def is_confirmed(self):\n        return self.height > 0\n\n    @property\n    def id(self):\n        return self.ref.id\n\n    @property\n    def hash(self):\n        return self.ref.hash\n\n    def get_julian_day(self, ledger):\n        if self._day is None and self.height > 0:\n            self._day = ledger.headers.estimated_julian_day(self.height)\n        return self._day\n\n    @property\n    def raw(self):\n        if self._raw is None:\n            self._raw = self._serialize()\n        return self._raw\n\n    @property\n    def raw_sans_segwit(self):\n        if self.is_segwit_flag:\n            if self._raw_sans_segwit is None:\n                self._raw_sans_segwit = self._serialize(sans_segwit=True)\n            return self._raw_sans_segwit\n        return self.raw\n\n    def _reset(self):\n        self._raw = None\n        self._raw_sans_segwit = None\n        self._raw_outputs = None\n        self.ref.reset()\n\n    @property\n    def inputs(self) -> ReadOnlyList[Input]:\n        return ReadOnlyList(self._inputs)\n\n    @property\n    def outputs(self) -> ReadOnlyList[Output]:\n        return ReadOnlyList(self._outputs)\n\n    def _add(self, existing_ios: List, new_ios: Iterable[InputOutput], reset=False) -> 'Transaction':\n        for txio in new_ios:\n            txio.tx_ref = self.ref\n            txio.position = len(existing_ios)\n            existing_ios.append(txio)\n        if reset:\n            self._reset()\n        return self\n\n    def add_inputs(self, inputs: Iterable[Input]) -> 'Transaction':\n        return self._add(self._inputs, inputs, True)\n\n    def add_outputs(self, outputs: Iterable[Output]) -> 'Transaction':\n        return self._add(self._outputs, outputs, True)\n\n    @property\n    def size(self) -> int:\n        \"\"\" Size in bytes of the entire transaction. \"\"\"\n        return len(self.raw)\n\n    @property\n    def base_size(self) -> int:\n        \"\"\" Size of transaction without inputs or outputs in bytes. \"\"\"\n        return (\n            self.size\n            - sum(txi.size for txi in self._inputs)\n            - sum(txo.size for txo in self._outputs)\n        )\n\n    @property\n    def input_sum(self):\n        return sum(i.amount for i in self.inputs if i.txo_ref.txo is not None)\n\n    @property\n    def output_sum(self):\n        return sum(o.amount for o in self.outputs)\n\n    @property\n    def net_account_balance(self) -> int:\n        balance = 0\n        for txi in self.inputs:\n            if txi.txo_ref.txo is None:\n                continue\n            if txi.is_my_input is True:\n                balance -= txi.amount\n            elif txi.is_my_input is None:\n                raise ValueError(\n                    \"Cannot access net_account_balance if inputs do not \"\n                    \"have is_my_input set properly.\"\n                )\n        for txo in self.outputs:\n            if txo.is_my_output is True:\n                balance += txo.amount\n            elif txo.is_my_output is None:\n                raise ValueError(\n                    \"Cannot access net_account_balance if outputs do not \"\n                    \"have is_my_output set properly.\"\n                )\n        return balance\n\n    @property\n    def fee(self) -> int:\n        return self.input_sum - self.output_sum\n\n    def get_base_fee(self, ledger) -> int:\n        \"\"\" Fee for base tx excluding inputs and outputs. \"\"\"\n        return self.base_size * ledger.fee_per_byte\n\n    def get_effective_input_sum(self, ledger) -> int:\n        \"\"\" Sum of input values *minus* the cost involved to spend them. \"\"\"\n        return sum(txi.amount - txi.get_fee(ledger) for txi in self._inputs)\n\n    def get_total_output_sum(self, ledger) -> int:\n        \"\"\" Sum of output values *plus* the cost involved to spend them. \"\"\"\n        return sum(txo.amount + txo.get_fee(ledger) for txo in self._outputs)\n\n    def _serialize(self, with_inputs: bool = True, sans_segwit: bool = False) -> bytes:\n        stream = BCDataStream()\n        stream.write_uint32(self.version)\n        if with_inputs:\n            stream.write_compact_size(len(self._inputs))\n            for txin in self._inputs:\n                txin.serialize_to(stream)\n        self._serialize_outputs(stream)\n        stream.write_uint32(self.locktime)\n        return stream.get_bytes()\n\n    def _serialize_for_signature(self, signing_input: int) -> bytes:\n        stream = BCDataStream()\n        stream.write_uint32(self.version)\n        stream.write_compact_size(len(self._inputs))\n        for i, txin in enumerate(self._inputs):\n            if signing_input == i:\n                if txin.script.is_script_hash:\n                    txin.serialize_to(stream, txin.script.values['script'].source)\n                else:\n                    assert txin.txo_ref.txo is not None\n                    txin.serialize_to(stream, txin.txo_ref.txo.script.source)\n            else:\n                txin.serialize_to(stream, b'')\n        self._serialize_outputs(stream)\n        stream.write_uint32(self.locktime)\n        stream.write_uint32(self.signature_hash_type(1))  # signature hash type: SIGHASH_ALL\n        return stream.get_bytes()\n\n    def _serialize_outputs(self, stream):\n        if self._raw_outputs is None:\n            self._raw_outputs = BCDataStream()\n            self._raw_outputs.write_compact_size(len(self._outputs))\n            for txout in self._outputs:\n                txout.serialize_to(self._raw_outputs)\n        stream.write(self._raw_outputs.get_bytes())\n\n    def _deserialize(self):\n        if self._raw is not None:\n            stream = BCDataStream(self._raw)\n            self.version = stream.read_uint32()\n            input_count = stream.read_compact_size()\n            if input_count == 0:\n                self.is_segwit_flag = stream.read_uint8()\n                input_count = stream.read_compact_size()\n            self._add(self._inputs, [\n                Input.deserialize_from(stream) for _ in range(input_count)\n            ])\n            output_count = stream.read_compact_size()\n            self._add(self._outputs, [\n                Output.deserialize_from(stream) for _ in range(output_count)\n            ])\n            if self.is_segwit_flag:\n                # drain witness portion of transaction\n                # too many witnesses for no crime\n                self.witnesses = []\n                for _ in range(input_count):\n                    for _ in range(stream.read_compact_size()):\n                        self.witnesses.append(stream.read(stream.read_compact_size()))\n            self.locktime = stream.read_uint32()\n\n    @classmethod\n    def ensure_all_have_same_ledger_and_wallet(\n            cls, funding_accounts: Iterable['Account'],\n            change_account: 'Account' = None) -> Tuple['Ledger', 'Wallet']:\n        ledger = wallet = None\n        for account in funding_accounts:\n            if ledger is None:\n                ledger = account.ledger\n                wallet = account.wallet\n            if ledger != account.ledger:\n                raise ValueError(\n                    'All funding accounts used to create a transaction must be on the same ledger.'\n                )\n            if wallet != account.wallet:\n                raise ValueError(\n                    'All funding accounts used to create a transaction must be from the same wallet.'\n                )\n        if change_account is not None:\n            if change_account.ledger != ledger:\n                raise ValueError('Change account must use same ledger as funding accounts.')\n            if change_account.wallet != wallet:\n                raise ValueError('Change account must use same wallet as funding accounts.')\n        if ledger is None:\n            raise ValueError('No ledger found.')\n        if wallet is None:\n            raise ValueError('No wallet found.')\n        return ledger, wallet\n\n    @classmethod\n    async def create(cls, inputs: Iterable[Input], outputs: Iterable[Output],\n                     funding_accounts: Iterable['Account'], change_account: 'Account',\n                     sign: bool = True):\n        \"\"\" Find optimal set of inputs when only outputs are provided; add change\n            outputs if only inputs are provided or if inputs are greater than outputs. \"\"\"\n\n        tx = cls() \\\n            .add_inputs(inputs) \\\n            .add_outputs(outputs)\n\n        ledger, _ = cls.ensure_all_have_same_ledger_and_wallet(funding_accounts, change_account)\n\n        # value of the outputs plus associated fees\n        cost = (\n            tx.get_base_fee(ledger) +\n            tx.get_total_output_sum(ledger)\n        )\n        # value of the inputs less the cost to spend those inputs\n        payment = tx.get_effective_input_sum(ledger)\n\n        try:\n\n            for _ in range(5):\n\n                if payment < cost:\n                    deficit = cost - payment\n                    spendables = await ledger.get_spendable_utxos(deficit, funding_accounts)\n                    if not spendables:\n                        raise InsufficientFundsError()\n                    payment += sum(s.effective_amount for s in spendables)\n                    tx.add_inputs(s.txi for s in spendables)\n\n                cost_of_change = (\n                    tx.get_base_fee(ledger) +\n                    Output.pay_pubkey_hash(COIN, NULL_HASH32).get_fee(ledger)\n                )\n                if payment > cost:\n                    change = payment - cost\n                    change_amount = change - cost_of_change\n                    if change_amount > DUST:\n                        change_address = await change_account.change.get_or_create_usable_address()\n                        change_hash160 = change_account.ledger.address_to_hash160(change_address)\n                        change_output = Output.pay_pubkey_hash(change_amount, change_hash160)\n                        change_output.is_internal_transfer = True\n                        tx.add_outputs([Output.pay_pubkey_hash(change_amount, change_hash160)])\n\n                if tx._outputs:\n                    break\n                # this condition and the outer range(5) loop cover an edge case\n                # whereby a single input is just enough to cover the fee and\n                # has some change left over, but the change left over is less\n                # than the cost_of_change: thus the input is completely\n                # consumed and no output is added, which is an invalid tx.\n                # to be able to spend this input we must increase the cost\n                # of the TX and run through the balance algorithm a second time\n                # adding an extra input and change output, making tx valid.\n                # we do this 5 times in case the other UTXOs added are also\n                # less than the fee, after 5 attempts we give up and go home\n                cost += cost_of_change + 1\n\n            if sign:\n                await tx.sign(funding_accounts)\n\n        except Exception as e:\n            log.exception('Failed to create transaction:')\n            await ledger.release_tx(tx)\n            raise e\n\n        return tx\n\n    @staticmethod\n    def signature_hash_type(hash_type):\n        return hash_type\n\n    async def sign(self, funding_accounts: Iterable['Account'], extra_keys: dict = None):\n        self._reset()\n        ledger, wallet = self.ensure_all_have_same_ledger_and_wallet(funding_accounts)\n        for i, txi in enumerate(self._inputs):\n            assert txi.script is not None\n            assert txi.txo_ref.txo is not None\n            txo_script = txi.txo_ref.txo.script\n            if txo_script.is_pay_pubkey_hash or txo_script.is_pay_script_hash:\n                if 'pubkey_hash' in txo_script.values:\n                    address = ledger.hash160_to_address(txo_script.values.get('pubkey_hash', ''))\n                    private_key = await ledger.get_private_key_for_address(wallet, address)\n                else:\n                    private_key = next(iter(extra_keys.values()))\n                assert private_key is not None, 'Cannot find private key for signing output.'\n                tx = self._serialize_for_signature(i)\n                txi.script.values['signature'] = \\\n                    private_key.sign(tx) + bytes((self.signature_hash_type(1),))\n                txi.script.values['pubkey'] = private_key.public_key.pubkey_bytes\n                txi.script.generate()\n            else:\n                raise NotImplementedError(\"Don't know how to spend this output.\")\n        self._reset()\n\n    @classmethod\n    def pay(cls, amount: int, address: bytes, funding_accounts: List['Account'], change_account: 'Account'):\n        ledger, _ = cls.ensure_all_have_same_ledger_and_wallet(funding_accounts, change_account)\n        output = Output.pay_pubkey_hash(amount, ledger.address_to_hash160(address))\n        return cls.create([], [output], funding_accounts, change_account)\n\n    @classmethod\n    def claim_create(\n            cls, name: str, claim: Claim, amount: int, holding_address: str,\n            funding_accounts: List['Account'], change_account: 'Account', signing_channel: Output = None):\n        ledger, _ = cls.ensure_all_have_same_ledger_and_wallet(funding_accounts, change_account)\n        claim_output = Output.pay_claim_name_pubkey_hash(\n            amount, name, claim, ledger.address_to_hash160(holding_address)\n        )\n        if signing_channel is not None:\n            claim_output.sign(signing_channel, b'placeholder txid:nout')\n        return cls.create([], [claim_output], funding_accounts, change_account, sign=False)\n\n    @classmethod\n    def claim_update(\n            cls, previous_claim: Output, claim: Claim, amount: int, holding_address: str,\n            funding_accounts: List['Account'], change_account: 'Account', signing_channel: Output = None):\n        ledger, _ = cls.ensure_all_have_same_ledger_and_wallet(funding_accounts, change_account)\n        updated_claim = Output.pay_update_claim_pubkey_hash(\n            amount, previous_claim.claim_name, previous_claim.claim_id,\n            claim, ledger.address_to_hash160(holding_address)\n        )\n        if signing_channel is not None:\n            updated_claim.sign(signing_channel, b'placeholder txid:nout')\n        else:\n            updated_claim.clear_signature()\n        return cls.create(\n            [Input.spend(previous_claim)], [updated_claim], funding_accounts, change_account, sign=False\n        )\n\n    @classmethod\n    def support(cls, claim_name: str, claim_id: str, amount: int, holding_address: str,\n                funding_accounts: List['Account'], change_account: 'Account', signing_channel: Output = None,\n                comment: str = None):\n        ledger, _ = cls.ensure_all_have_same_ledger_and_wallet(funding_accounts, change_account)\n        if signing_channel is not None or comment is not None:\n            support = Support()\n            if comment is not None:\n                support.comment = comment\n            support_output = Output.pay_support_data_pubkey_hash(\n                amount, claim_name, claim_id, support, ledger.address_to_hash160(holding_address)\n            )\n            if signing_channel is not None:\n                support_output.sign(signing_channel, b'placeholder txid:nout')\n        else:\n            support_output = Output.pay_support_pubkey_hash(\n                amount, claim_name, claim_id, ledger.address_to_hash160(holding_address)\n            )\n        return cls.create([], [support_output], funding_accounts, change_account, sign=False)\n\n    @classmethod\n    def purchase(cls, claim_id: str, amount: int, merchant_address: bytes,\n                 funding_accounts: List['Account'], change_account: 'Account'):\n        ledger, _ = cls.ensure_all_have_same_ledger_and_wallet(funding_accounts, change_account)\n        payment = Output.pay_pubkey_hash(amount, ledger.address_to_hash160(merchant_address))\n        data = Output.add_purchase_data(Purchase(claim_id))\n        return cls.create([], [payment, data], funding_accounts, change_account)\n\n    @classmethod\n    async def spend_time_lock(cls, time_locked_txo: Output, script: bytes, account: 'Account'):\n        txi = Input.spend_time_lock(time_locked_txo, script)\n        txi.sequence = 0xFFFFFFFE\n        tx = await cls.create([txi], [], [account], account, sign=False)\n        tx.locktime = txi.script.values['script'].values['height']\n        tx._reset()\n        return tx\n\n    @property\n    def my_inputs(self):\n        for txi in self.inputs:\n            if txi.txo_ref.txo is not None and txi.txo_ref.txo.is_my_output:\n                yield txi\n\n    def _filter_my_outputs(self, f):\n        for txo in self.outputs:\n            if txo.is_my_output and f(txo.script):\n                yield txo\n\n    def _filter_other_outputs(self, f):\n        for txo in self.outputs:\n            if not txo.is_my_output and f(txo.script):\n                yield txo\n\n    def _filter_any_outputs(self, f):\n        for txo in self.outputs:\n            if f(txo):\n                yield txo\n\n    @property\n    def my_claim_outputs(self):\n        return self._filter_my_outputs(lambda s: s.is_claim_name)\n\n    @property\n    def my_update_outputs(self):\n        return self._filter_my_outputs(lambda s: s.is_update_claim)\n\n    @property\n    def my_support_outputs(self):\n        return self._filter_my_outputs(lambda s: s.is_support_claim)\n\n    @property\n    def any_purchase_outputs(self):\n        return self._filter_any_outputs(lambda o: o.purchase is not None)\n\n    @property\n    def other_support_outputs(self):\n        return self._filter_other_outputs(lambda s: s.is_support_claim)\n\n    @property\n    def my_abandon_outputs(self):\n        for txi in self.inputs:\n            abandon = txi.txo_ref.txo\n            if abandon is not None and abandon.is_my_output and abandon.script.is_claim_involved:\n                is_update = False\n                if abandon.script.is_claim_name or abandon.script.is_update_claim:\n                    for update in self.my_update_outputs:\n                        if abandon.claim_id == update.claim_id:\n                            is_update = True\n                            break\n                if not is_update:\n                    yield abandon\n"
  },
  {
    "path": "lbry/wallet/udp.py",
    "content": "import asyncio\nimport struct\nfrom time import perf_counter\nimport logging\nfrom typing import Optional, Tuple, NamedTuple\nfrom lbry.utils import LRUCache, is_valid_public_ipv4\nfrom lbry.schema.attrs import country_str_to_int, country_int_to_str\n# from prometheus_client import Counter\n\n\nlog = logging.getLogger(__name__)\n_MAGIC = 1446058291  # genesis blocktime (which is actually wrong)\n# ping_count_metric = Counter(\"ping_count\", \"Number of pings received\", namespace='wallet_server_status')\n_PAD_BYTES = b'\\x00' * 64\n\n\nPROTOCOL_VERSION = 1\n\n\nclass SPVPing(NamedTuple):\n    magic: int\n    protocol_version: int\n    pad_bytes: bytes\n\n    def encode(self):\n        return struct.pack(b'!lB64s', *self)  # pylint: disable=not-an-iterable\n\n    @staticmethod\n    def make() -> bytes:\n        return SPVPing(_MAGIC, PROTOCOL_VERSION, _PAD_BYTES).encode()\n\n    @classmethod\n    def decode(cls, packet: bytes):\n        decoded = cls(*struct.unpack(b'!lB64s', packet[:69]))\n        if decoded.magic != _MAGIC:\n            raise ValueError(\"invalid magic bytes\")\n        return decoded\n\n\nPONG_ENCODING = b'!BBL32s4sH'\n\n\nclass SPVPong(NamedTuple):\n    protocol_version: int\n    flags: int\n    height: int\n    tip: bytes\n    source_address_raw: bytes\n    country: int\n\n    def encode(self):\n        return struct.pack(PONG_ENCODING, *self)  # pylint: disable=not-an-iterable\n\n    @staticmethod\n    def encode_address(address: str):\n        return bytes(int(b) for b in address.split(\".\"))\n\n    @classmethod\n    def make(cls, flags: int, height: int, tip: bytes, source_address: str, country: str) -> bytes:\n        return SPVPong(\n            PROTOCOL_VERSION, flags, height, tip,\n            cls.encode_address(source_address),\n            country_str_to_int(country)\n        ).encode()\n\n    @classmethod\n    def make_sans_source_address(cls, flags: int, height: int, tip: bytes, country: str) -> Tuple[bytes, bytes]:\n        pong = cls.make(flags, height, tip, '0.0.0.0', country)\n        return pong[:38], pong[42:]\n\n    @classmethod\n    def decode(cls, packet: bytes):\n        return cls(*struct.unpack(PONG_ENCODING, packet[:44]))\n\n    @property\n    def available(self) -> bool:\n        return (self.flags & 0b00000001) > 0\n\n    @property\n    def ip_address(self) -> str:\n        return \".\".join(map(str, self.source_address_raw))\n\n    @property\n    def country_name(self):\n        return country_int_to_str(self.country)\n\n    def __repr__(self) -> str:\n        return f\"SPVPong(external_ip={self.ip_address}, version={self.protocol_version}, \" \\\n               f\"available={'True' if self.flags & 1 > 0 else 'False'},\" \\\n               f\" height={self.height}, tip={self.tip[::-1].hex()}, country={self.country_name})\"\n\n\nclass SPVServerStatusProtocol(asyncio.DatagramProtocol):\n\n    def __init__(\n        self, height: int, tip: bytes, country: str,\n        throttle_cache_size: int = 1024, throttle_reqs_per_sec: int = 10,\n        allow_localhost: bool = False, allow_lan: bool = False\n    ):\n        super().__init__()\n        self.transport: Optional[asyncio.transports.DatagramTransport] = None\n        self._height = height\n        self._tip = tip\n        self._flags = 0\n        self._country = country\n        self._left_cache = self._right_cache = None\n        self.update_cached_response()\n        self._throttle = LRUCache(throttle_cache_size)\n        self._should_log = LRUCache(throttle_cache_size)\n        self._min_delay = 1 / throttle_reqs_per_sec\n        self._allow_localhost = allow_localhost\n        self._allow_lan = allow_lan\n        self.closed = asyncio.Event()\n\n    def update_cached_response(self):\n        self._left_cache, self._right_cache = SPVPong.make_sans_source_address(\n            self._flags, max(0, self._height), self._tip, self._country\n        )\n\n    def set_unavailable(self):\n        self._flags &= 0b11111110\n        self.update_cached_response()\n\n    def set_available(self):\n        self._flags |= 0b00000001\n        self.update_cached_response()\n\n    def set_height(self, height: int, tip: bytes):\n        self._height, self._tip = height, tip\n        self.update_cached_response()\n\n    def should_throttle(self, host: str):\n        now = perf_counter()\n        last_requested = self._throttle.get(host, default=0)\n        self._throttle[host] = now\n        if now - last_requested < self._min_delay:\n            log_cnt = self._should_log.get(host, default=0) + 1\n            if log_cnt % 100 == 0:\n                log.warning(\"throttle spv status to %s\", host)\n            self._should_log[host] = log_cnt\n            return True\n        return False\n\n    def make_pong(self, host):\n        return self._left_cache + SPVPong.encode_address(host) + self._right_cache\n\n    def datagram_received(self, data: bytes, addr: Tuple[str, int]):\n        if self.should_throttle(addr[0]):\n            return\n        try:\n            SPVPing.decode(data)\n        except (ValueError, struct.error, AttributeError, TypeError):\n            # log.exception(\"derp\")\n            return\n        if addr[1] >= 1024 and is_valid_public_ipv4(\n                addr[0], allow_localhost=self._allow_localhost, allow_lan=self._allow_lan):\n            self.transport.sendto(self.make_pong(addr[0]), addr)\n        else:\n            log.warning(\"odd packet from %s:%i\", addr[0], addr[1])\n        # ping_count_metric.inc()\n\n    def connection_made(self, transport) -> None:\n        self.transport = transport\n        self.closed.clear()\n\n    def connection_lost(self, exc: Optional[Exception]) -> None:\n        self.transport = None\n        self.closed.set()\n\n    async def close(self):\n        if self.transport:\n            self.transport.close()\n        await self.closed.wait()\n\n\nclass StatusServer:\n    def __init__(self):\n        self._protocol: Optional[SPVServerStatusProtocol] = None\n\n    async def start(self, height: int, tip: bytes, country: str, interface: str, port: int, allow_lan: bool = False):\n        if self.is_running:\n            return\n        loop = asyncio.get_event_loop()\n        interface = interface if interface.lower() != 'localhost' else '127.0.0.1'\n        self._protocol = SPVServerStatusProtocol(\n            height, tip, country, allow_localhost=interface == '127.0.0.1', allow_lan=allow_lan\n        )\n        await loop.create_datagram_endpoint(lambda: self._protocol, (interface, port))\n        log.info(\"started udp status server on %s:%i\", interface, port)\n\n    async def stop(self):\n        if self.is_running:\n            await self._protocol.close()\n            self._protocol = None\n\n    @property\n    def is_running(self):\n        return self._protocol is not None\n\n    def set_unavailable(self):\n        if self.is_running:\n            self._protocol.set_unavailable()\n\n    def set_available(self):\n        if self.is_running:\n            self._protocol.set_available()\n\n    def set_height(self, height: int, tip: bytes):\n        if self.is_running:\n            self._protocol.set_height(height, tip)\n\n\nclass SPVStatusClientProtocol(asyncio.DatagramProtocol):\n\n    def __init__(self, responses: asyncio.Queue):\n        super().__init__()\n        self.transport: Optional[asyncio.transports.DatagramTransport] = None\n        self.responses = responses\n        self._ping_packet = SPVPing.make()\n\n    def datagram_received(self, data: bytes, addr: Tuple[str, int]):\n        try:\n            self.responses.put_nowait(((addr, perf_counter()), SPVPong.decode(data)))\n        except (ValueError, struct.error, AttributeError, TypeError, RuntimeError):\n            return\n\n    def connection_made(self, transport) -> None:\n        self.transport = transport\n\n    def connection_lost(self, exc: Optional[Exception]) -> None:\n        self.transport = None\n        log.info(\"closed udp spv server selection client\")\n\n    def ping(self, server: Tuple[str, int]):\n        self.transport.sendto(self._ping_packet, server)\n\n    def close(self):\n        # log.info(\"close udp client\")\n        if self.transport:\n            self.transport.close()\n"
  },
  {
    "path": "lbry/wallet/usage_payment.py",
    "content": "import asyncio\nimport logging\n\nfrom lbry.error import (\n    InsufficientFundsError,\n    ServerPaymentFeeAboveMaxAllowedError,\n    ServerPaymentInvalidAddressError,\n    ServerPaymentWalletLockedError\n)\nfrom lbry.wallet.dewies import lbc_to_dewies\nfrom lbry.wallet.stream import StreamController\nfrom lbry.wallet.transaction import Output, Transaction\n\nlog = logging.getLogger(__name__)\n\n\nclass WalletServerPayer:\n    def __init__(self, payment_period=24 * 60 * 60, max_fee='1.0', analytics_manager=None):\n        self.ledger = None\n        self.wallet = None\n        self.running = False\n        self.task = None\n        self.payment_period = payment_period\n        self.analytics_manager = analytics_manager\n        self.max_fee = max_fee\n        self._on_payment_controller = StreamController()\n        self.on_payment = self._on_payment_controller.stream\n        self.on_payment.listen(None, on_error=lambda e: log.warning(e.args[0]))\n\n    async def pay(self):\n        while self.running:\n            try:\n                await self._pay()\n            except (asyncio.TimeoutError, ConnectionError):\n                if not self.running:\n                    break\n                delay = max(self.payment_period / 24, 10)\n                log.warning(\"Payement failed. Will retry after %g seconds.\", delay)\n                asyncio.sleep(delay)\n            except BaseException as e:\n                if not isinstance(e, asyncio.CancelledError):\n                    log.exception(\"Unexpected exception. Payment task exiting early.\")\n                self.running = False\n                raise\n\n    async def _pay(self):\n        while self.running:\n            await asyncio.sleep(self.payment_period)\n            features = await self.ledger.network.get_server_features()\n            log.debug(\"pay loop: received server features: %s\", str(features))\n            address = features['payment_address']\n            amount = str(features['daily_fee'])\n            if not address or not amount:\n                log.debug(\"pay loop: no address or no amount\")\n                continue\n\n            if not self.ledger.is_pubkey_address(address):\n                log.info(\"pay loop: address not pubkey\")\n                self._on_payment_controller.add_error(ServerPaymentInvalidAddressError(address))\n                continue\n\n            if self.wallet.is_locked:\n                log.info(\"pay loop: wallet is locked\")\n                self._on_payment_controller.add_error(ServerPaymentWalletLockedError())\n                continue\n\n            amount = lbc_to_dewies(features['daily_fee'])  # check that this is in lbc and not dewies\n            limit = lbc_to_dewies(self.max_fee)\n            if amount > limit:\n                log.info(\"pay loop: amount (%d) > limit (%d)\", amount, limit)\n                self._on_payment_controller.add_error(\n                    ServerPaymentFeeAboveMaxAllowedError(features['daily_fee'], self.max_fee)\n                )\n                continue\n\n            try:\n                tx = await Transaction.create(\n                    [],\n                    [Output.pay_pubkey_hash(amount, self.ledger.address_to_hash160(address))],\n                    self.wallet.get_accounts_or_all(None),\n                    self.wallet.get_account_or_default(None)\n                )\n            except InsufficientFundsError:\n                self._on_payment_controller.add_error(InsufficientFundsError())\n                continue\n\n            await self.ledger.broadcast_or_release(tx, blocking=True)\n            if self.analytics_manager:\n                await self.analytics_manager.send_credits_sent()\n            self._on_payment_controller.add(tx)\n\n    async def start(self, ledger=None, wallet=None):\n        if lbc_to_dewies(self.max_fee) < 1:\n            return\n        self.ledger = ledger\n        self.wallet = wallet\n        self.running = True\n        self.task = asyncio.ensure_future(self.pay())\n        self.task.add_done_callback(self._done_callback)\n\n    def _done_callback(self, f):\n        if f.cancelled():\n            reason = \"Cancelled\"\n        elif f.exception():\n            reason = f'Exception: {f.exception()}'\n        elif not self.running:\n            reason = \"Stopped\"\n        else:\n            reason = \"\"\n        log.info(\"Stopping wallet server payments. %s\", reason)\n\n    async def stop(self):\n        if self.running:\n            self.running = False\n            self.task.cancel()\n"
  },
  {
    "path": "lbry/wallet/util.py",
    "content": "import re\nfrom typing import TypeVar, Sequence, Optional\nfrom .constants import COIN\n\n\ndef date_to_julian_day(d):\n    return d.toordinal() + 1721424.5\n\n\ndef coins_to_satoshis(coins):\n    if not isinstance(coins, str):\n        raise ValueError(\"{coins} must be a string\")\n    result = re.search(r'^(\\d{1,10})\\.(\\d{1,8})$', coins)\n    if result is not None:\n        whole, fractional = result.groups()\n        return int(whole+fractional.ljust(8, \"0\"))\n    raise ValueError(\"'{lbc}' is not a valid coin decimal\")\n\n\ndef satoshis_to_coins(satoshis):\n    coins = '{:.8f}'.format(satoshis / COIN).rstrip('0')\n    if coins.endswith('.'):\n        return coins+'0'\n    else:\n        return coins\n\n\nT = TypeVar('T')\n\n\nclass ReadOnlyList(Sequence[T]):\n\n    def __init__(self, lst):\n        self.lst = lst\n\n    def __getitem__(self, key):\n        return self.lst[key]\n\n    def __len__(self) -> int:\n        return len(self.lst)\n\n\ndef subclass_tuple(name, base):\n    return type(name, (base,), {'__slots__': ()})\n\n\nclass cachedproperty:\n\n    def __init__(self, f):\n        self.f = f\n\n    def __get__(self, obj, objtype):\n        obj = obj or objtype\n        value = self.f(obj)\n        setattr(obj, self.f.__name__, value)\n        return value\n\n\nclass ArithUint256:\n    # https://github.com/bitcoin/bitcoin/blob/master/src/arith_uint256.cpp\n\n    __slots__ = '_value', '_compact'\n\n    def __init__(self, value: int) -> None:\n        self._value = value\n        self._compact: Optional[int] = None\n\n    @classmethod\n    def from_compact(cls, compact) -> 'ArithUint256':\n        size = compact >> 24\n        word = compact & 0x007fffff\n        if size <= 3:\n            return cls(word >> 8 * (3 - size))\n        else:\n            return cls(word << 8 * (size - 3))\n\n    @property\n    def value(self) -> int:\n        return self._value\n\n    @property\n    def compact(self) -> int:\n        if self._compact is None:\n            self._compact = self._calculate_compact()\n        return self._compact\n\n    @property\n    def negative(self) -> int:\n        return self._calculate_compact(negative=True)\n\n    @property\n    def bits(self) -> int:\n        \"\"\" Returns the position of the highest bit set plus one. \"\"\"\n        bits = bin(self._value)[2:]\n        for i, d in enumerate(bits):\n            if d:\n                return (len(bits) - i) + 1\n        return 0\n\n    @property\n    def low64(self) -> int:\n        return self._value & 0xffffffffffffffff\n\n    def _calculate_compact(self, negative=False) -> int:\n        size = (self.bits + 7) // 8\n        if size <= 3:\n            compact = self.low64 << 8 * (3 - size)\n        else:\n            compact = ArithUint256(self._value >> 8 * (size - 3)).low64\n        # The 0x00800000 bit denotes the sign.\n        # Thus, if it is already set, divide the mantissa by 256 and increase the exponent.\n        if compact & 0x00800000:\n            compact >>= 8\n            size += 1\n        assert (compact & ~0x007fffff) == 0\n        assert size < 256\n        compact |= size << 24\n        if negative and compact & 0x007fffff:\n            compact |= 0x00800000\n        return compact\n\n    def __mul__(self, x):\n        # Take the mod because we are limited to an unsigned 256 bit number\n        return ArithUint256((self._value * x) % 2 ** 256)\n\n    def __truediv__(self, x):\n        return ArithUint256(int(self._value / x))\n\n    def __gt__(self, other):\n        return self._value > other\n\n    def __lt__(self, other):\n        return self._value < other\n"
  },
  {
    "path": "lbry/wallet/wallet.py",
    "content": "import os\nimport time\nimport stat\nimport json\nimport zlib\nimport typing\nimport logging\nfrom typing import List, Sequence, MutableSequence, Optional\nfrom collections import UserDict\nfrom hashlib import sha256\nfrom operator import attrgetter\nfrom lbry.crypto.crypt import better_aes_encrypt, better_aes_decrypt\nfrom lbry.error import InvalidPasswordError\nfrom .account import Account\n\nif typing.TYPE_CHECKING:\n    from lbry.wallet.manager import WalletManager\n    from lbry.wallet.ledger import Ledger\n\n\nlog = logging.getLogger(__name__)\n\nENCRYPT_ON_DISK = 'encrypt-on-disk'\n\n\nclass TimestampedPreferences(UserDict):\n\n    def __init__(self, d: dict = None):\n        super().__init__()\n        if d is not None:\n            self.data = d.copy()\n\n    def __getitem__(self, key):\n        return self.data[key]['value']\n\n    def __setitem__(self, key, value):\n        self.data[key] = {\n            'value': value,\n            'ts': int(time.time())\n        }\n\n    def __repr__(self):\n        return repr(self.to_dict_without_ts())\n\n    def to_dict_without_ts(self):\n        return {\n            key: value['value'] for key, value in self.data.items()\n        }\n\n    @property\n    def hash(self):\n        return sha256(json.dumps(self.data).encode()).digest()\n\n    def merge(self, other: dict):\n        for key, value in other.items():\n            if key in self.data and value['ts'] < self.data[key]['ts']:\n                continue\n            self.data[key] = value\n\n\nclass Wallet:\n    \"\"\" The primary role of Wallet is to encapsulate a collection\n        of accounts (seed/private keys) and the spending rules / settings\n        for the coins attached to those accounts. Wallets are represented\n        by physical files on the filesystem.\n    \"\"\"\n\n    preferences: TimestampedPreferences\n    encryption_password: Optional[str]\n\n    def __init__(self, name: str = 'Wallet', accounts: MutableSequence['Account'] = None,\n                 storage: 'WalletStorage' = None, preferences: dict = None) -> None:\n        self.name = name\n        self.accounts = accounts or []\n        self.storage = storage or WalletStorage()\n        self.preferences = TimestampedPreferences(preferences or {})\n        self.encryption_password = None\n        self.id = self.get_id()\n\n    def get_id(self):\n        return os.path.basename(self.storage.path) if self.storage.path else self.name\n\n    def add_account(self, account: 'Account'):\n        self.accounts.append(account)\n\n    def generate_account(self, ledger: 'Ledger') -> 'Account':\n        return Account.generate(ledger, self)\n\n    @property\n    def default_account(self) -> Optional['Account']:\n        for account in self.accounts:\n            return account\n        return None\n\n    def get_account_or_default(self, account_id: str) -> Optional['Account']:\n        if account_id is None:\n            return self.default_account\n        return self.get_account_or_error(account_id)\n\n    def get_account_or_error(self, account_id: str) -> 'Account':\n        for account in self.accounts:\n            if account.id == account_id:\n                return account\n        raise ValueError(f\"Couldn't find account: {account_id}.\")\n\n    def get_accounts_or_all(self, account_ids: List[str]) -> Sequence['Account']:\n        return [\n            self.get_account_or_error(account_id)\n            for account_id in account_ids\n        ] if account_ids else self.accounts\n\n    async def get_detailed_accounts(self, **kwargs):\n        accounts = []\n        for i, account in enumerate(self.accounts):\n            details = await account.get_details(**kwargs)\n            details['is_default'] = i == 0\n            accounts.append(details)\n        return accounts\n\n    @classmethod\n    def from_storage(cls, storage: 'WalletStorage', manager: 'WalletManager') -> 'Wallet':\n        json_dict = storage.read()\n        wallet = cls(\n            name=json_dict.get('name', 'Wallet'),\n            preferences=json_dict.get('preferences', {}),\n            storage=storage\n        )\n        account_dicts: Sequence[dict] = json_dict.get('accounts', [])\n        for account_dict in account_dicts:\n            ledger = manager.get_or_create_ledger(account_dict['ledger'])\n            Account.from_dict(ledger, wallet, account_dict)\n        return wallet\n\n    def to_dict(self, encrypt_password: str = None):\n        return {\n            'version': WalletStorage.LATEST_VERSION,\n            'name': self.name,\n            'preferences': self.preferences.data,\n            'accounts': [a.to_dict(encrypt_password) for a in self.accounts]\n        }\n\n    def to_json(self):\n        assert not self.is_locked, \"Cannot serialize a wallet with locked/encrypted accounts.\"\n        return json.dumps(self.to_dict())\n\n    def save(self):\n        if self.preferences.get(ENCRYPT_ON_DISK, False):\n            if self.encryption_password is not None:\n                return self.storage.write(self.to_dict(encrypt_password=self.encryption_password))\n            elif not self.is_locked:\n                log.warning(\n                    \"Disk encryption requested but no password available for encryption. \"\n                    \"Resetting encryption preferences and saving wallet in an unencrypted state.\"\n                )\n                self.preferences[ENCRYPT_ON_DISK] = False\n        return self.storage.write(self.to_dict())\n\n    @property\n    def hash(self) -> bytes:\n        h = sha256()\n        if self.is_encrypted:\n            assert self.encryption_password is not None, \\\n                \"Encryption is enabled but no password is available, cannot generate hash.\"\n            h.update(self.encryption_password.encode())\n        h.update(self.preferences.hash)\n        for account in sorted(self.accounts, key=attrgetter('id')):\n            h.update(account.hash)\n        return h.digest()\n\n    def pack(self, password):\n        assert not self.is_locked, \"Cannot pack a wallet with locked/encrypted accounts.\"\n        new_data_compressed = zlib.compress(self.to_json().encode())\n        return better_aes_encrypt(password, new_data_compressed)\n\n    @classmethod\n    def unpack(cls, password, encrypted):\n        decrypted = better_aes_decrypt(password, encrypted)\n        try:\n            decompressed = zlib.decompress(decrypted)\n        except zlib.error as e:\n            if \"incorrect header check\" in e.args[0].lower():\n                raise InvalidPasswordError()\n            if \"unknown compression method\" in e.args[0].lower():\n                raise InvalidPasswordError()\n            if \"invalid window size\" in e.args[0].lower():\n                raise InvalidPasswordError()\n            raise\n        return json.loads(decompressed)\n\n    def merge(self, manager: 'WalletManager',\n              password: str, data: str) -> (List['Account'], List['Account']):\n        assert not self.is_locked, \"Cannot sync apply on a locked wallet.\"\n        added_accounts, merged_accounts = [], []\n        if password is None:\n            decrypted_data = json.loads(data)\n        else:\n            decrypted_data = self.unpack(password, data)\n        self.preferences.merge(decrypted_data.get('preferences', {}))\n        for account_dict in decrypted_data['accounts']:\n            ledger = manager.get_or_create_ledger(account_dict['ledger'])\n            _, _, pubkey = Account.keys_from_dict(ledger, account_dict)\n            account_id = pubkey.address\n            local_match = None\n            for local_account in self.accounts:\n                if account_id == local_account.id:\n                    local_match = local_account\n                    break\n            if local_match is not None:\n                local_match.merge(account_dict)\n                merged_accounts.append(local_match)\n            else:\n                new_account = Account.from_dict(ledger, self, account_dict)\n                added_accounts.append(new_account)\n        return added_accounts, merged_accounts\n\n    @property\n    def is_locked(self) -> bool:\n        for account in self.accounts:\n            if account.encrypted:\n                return True\n        return False\n\n    async def unlock(self, password):\n        for account in self.accounts:\n            if account.encrypted:\n                if not account.decrypt(password):\n                    return False\n                await account.deterministic_channel_keys.ensure_cache_primed()\n        self.encryption_password = password\n        return True\n\n    def lock(self):\n        assert self.encryption_password is not None, \"Cannot lock an unencrypted wallet, encrypt first.\"\n        for account in self.accounts:\n            if not account.encrypted:\n                account.encrypt(self.encryption_password)\n        return True\n\n    @property\n    def is_encrypted(self) -> bool:\n        # either its locked or it was unlocked using a password.\n        # if its set to encrypt on preferences but isnt encrypted and no password was given so far,\n        # then its not encrypted\n        return self.is_locked or (\n            self.preferences.get(ENCRYPT_ON_DISK, False) and self.encryption_password is not None)\n\n    def decrypt(self):\n        assert not self.is_locked, \"Cannot decrypt a locked wallet, unlock first.\"\n        self.preferences[ENCRYPT_ON_DISK] = False\n        self.save()\n        return True\n\n    def encrypt(self, password):\n        assert not self.is_locked, \"Cannot re-encrypt a locked wallet, unlock first.\"\n        assert password, \"Cannot encrypt with blank password.\"\n        self.encryption_password = password\n        self.preferences[ENCRYPT_ON_DISK] = True\n        self.save()\n        return True\n\n\nclass WalletStorage:\n\n    LATEST_VERSION = 1\n\n    def __init__(self, path=None, default=None):\n        self.path = path\n        self._default = default or {\n            'version': self.LATEST_VERSION,\n            'name': 'My Wallet',\n            'preferences': {},\n            'accounts': []\n        }\n\n    def read(self):\n        if self.path and os.path.exists(self.path):\n            with open(self.path, 'r') as f:\n                json_data = f.read()\n                json_dict = json.loads(json_data)\n                if json_dict.get('version') == self.LATEST_VERSION and \\\n                        set(json_dict) == set(self._default):\n                    return json_dict\n                else:\n                    return self.upgrade(json_dict)\n        else:\n            return self._default.copy()\n\n    def upgrade(self, json_dict):\n        json_dict = json_dict.copy()\n        version = json_dict.pop('version', -1)\n        if version == -1:\n            pass\n        upgraded = self._default.copy()\n        upgraded.update(json_dict)\n        return json_dict\n\n    def write(self, json_dict):\n\n        json_data = json.dumps(json_dict, indent=4, sort_keys=True)\n        if self.path is None:\n            return json_data\n\n        temp_path = \"{}.tmp.{}\".format(self.path, os.getpid())\n        with open(temp_path, \"w\") as f:\n            f.write(json_data)\n            f.flush()\n            os.fsync(f.fileno())\n\n        if os.path.exists(self.path):\n            mode = os.stat(self.path).st_mode\n        else:\n            mode = stat.S_IREAD | stat.S_IWRITE\n        try:\n            os.rename(temp_path, self.path)\n        except Exception:  # pylint: disable=broad-except\n            os.remove(self.path)\n            os.rename(temp_path, self.path)\n        os.chmod(self.path, mode)\n"
  },
  {
    "path": "lbry/wallet/words/__init__.py",
    "content": ""
  },
  {
    "path": "lbry/wallet/words/chinese_simplified.py",
    "content": "words = [\n'的',\n'一',\n'是',\n'在',\n'不',\n'了',\n'有',\n'和',\n'人',\n'这',\n'中',\n'大',\n'为',\n'上',\n'个',\n'国',\n'我',\n'以',\n'要',\n'他',\n'时',\n'来',\n'用',\n'们',\n'生',\n'到',\n'作',\n'地',\n'于',\n'出',\n'就',\n'分',\n'对',\n'成',\n'会',\n'可',\n'主',\n'发',\n'年',\n'动',\n'同',\n'工',\n'也',\n'能',\n'下',\n'过',\n'子',\n'说',\n'产',\n'种',\n'面',\n'而',\n'方',\n'后',\n'多',\n'定',\n'行',\n'学',\n'法',\n'所',\n'民',\n'得',\n'经',\n'十',\n'三',\n'之',\n'进',\n'着',\n'等',\n'部',\n'度',\n'家',\n'电',\n'力',\n'里',\n'如',\n'水',\n'化',\n'高',\n'自',\n'二',\n'理',\n'起',\n'小',\n'物',\n'现',\n'实',\n'加',\n'量',\n'都',\n'两',\n'体',\n'制',\n'机',\n'当',\n'使',\n'点',\n'从',\n'业',\n'本',\n'去',\n'把',\n'性',\n'好',\n'应',\n'开',\n'它',\n'合',\n'还',\n'因',\n'由',\n'其',\n'些',\n'然',\n'前',\n'外',\n'天',\n'政',\n'四',\n'日',\n'那',\n'社',\n'义',\n'事',\n'平',\n'形',\n'相',\n'全',\n'表',\n'间',\n'样',\n'与',\n'关',\n'各',\n'重',\n'新',\n'线',\n'内',\n'数',\n'正',\n'心',\n'反',\n'你',\n'明',\n'看',\n'原',\n'又',\n'么',\n'利',\n'比',\n'或',\n'但',\n'质',\n'气',\n'第',\n'向',\n'道',\n'命',\n'此',\n'变',\n'条',\n'只',\n'没',\n'结',\n'解',\n'问',\n'意',\n'建',\n'月',\n'公',\n'无',\n'系',\n'军',\n'很',\n'情',\n'者',\n'最',\n'立',\n'代',\n'想',\n'已',\n'通',\n'并',\n'提',\n'直',\n'题',\n'党',\n'程',\n'展',\n'五',\n'果',\n'料',\n'象',\n'员',\n'革',\n'位',\n'入',\n'常',\n'文',\n'总',\n'次',\n'品',\n'式',\n'活',\n'设',\n'及',\n'管',\n'特',\n'件',\n'长',\n'求',\n'老',\n'头',\n'基',\n'资',\n'边',\n'流',\n'路',\n'级',\n'少',\n'图',\n'山',\n'统',\n'接',\n'知',\n'较',\n'将',\n'组',\n'见',\n'计',\n'别',\n'她',\n'手',\n'角',\n'期',\n'根',\n'论',\n'运',\n'农',\n'指',\n'几',\n'九',\n'区',\n'强',\n'放',\n'决',\n'西',\n'被',\n'干',\n'做',\n'必',\n'战',\n'先',\n'回',\n'则',\n'任',\n'取',\n'据',\n'处',\n'队',\n'南',\n'给',\n'色',\n'光',\n'门',\n'即',\n'保',\n'治',\n'北',\n'造',\n'百',\n'规',\n'热',\n'领',\n'七',\n'海',\n'口',\n'东',\n'导',\n'器',\n'压',\n'志',\n'世',\n'金',\n'增',\n'争',\n'济',\n'阶',\n'油',\n'思',\n'术',\n'极',\n'交',\n'受',\n'联',\n'什',\n'认',\n'六',\n'共',\n'权',\n'收',\n'证',\n'改',\n'清',\n'美',\n'再',\n'采',\n'转',\n'更',\n'单',\n'风',\n'切',\n'打',\n'白',\n'教',\n'速',\n'花',\n'带',\n'安',\n'场',\n'身',\n'车',\n'例',\n'真',\n'务',\n'具',\n'万',\n'每',\n'目',\n'至',\n'达',\n'走',\n'积',\n'示',\n'议',\n'声',\n'报',\n'斗',\n'完',\n'类',\n'八',\n'离',\n'华',\n'名',\n'确',\n'才',\n'科',\n'张',\n'信',\n'马',\n'节',\n'话',\n'米',\n'整',\n'空',\n'元',\n'况',\n'今',\n'集',\n'温',\n'传',\n'土',\n'许',\n'步',\n'群',\n'广',\n'石',\n'记',\n'需',\n'段',\n'研',\n'界',\n'拉',\n'林',\n'律',\n'叫',\n'且',\n'究',\n'观',\n'越',\n'织',\n'装',\n'影',\n'算',\n'低',\n'持',\n'音',\n'众',\n'书',\n'布',\n'复',\n'容',\n'儿',\n'须',\n'际',\n'商',\n'非',\n'验',\n'连',\n'断',\n'深',\n'难',\n'近',\n'矿',\n'千',\n'周',\n'委',\n'素',\n'技',\n'备',\n'半',\n'办',\n'青',\n'省',\n'列',\n'习',\n'响',\n'约',\n'支',\n'般',\n'史',\n'感',\n'劳',\n'便',\n'团',\n'往',\n'酸',\n'历',\n'市',\n'克',\n'何',\n'除',\n'消',\n'构',\n'府',\n'称',\n'太',\n'准',\n'精',\n'值',\n'号',\n'率',\n'族',\n'维',\n'划',\n'选',\n'标',\n'写',\n'存',\n'候',\n'毛',\n'亲',\n'快',\n'效',\n'斯',\n'院',\n'查',\n'江',\n'型',\n'眼',\n'王',\n'按',\n'格',\n'养',\n'易',\n'置',\n'派',\n'层',\n'片',\n'始',\n'却',\n'专',\n'状',\n'育',\n'厂',\n'京',\n'识',\n'适',\n'属',\n'圆',\n'包',\n'火',\n'住',\n'调',\n'满',\n'县',\n'局',\n'照',\n'参',\n'红',\n'细',\n'引',\n'听',\n'该',\n'铁',\n'价',\n'严',\n'首',\n'底',\n'液',\n'官',\n'德',\n'随',\n'病',\n'苏',\n'失',\n'尔',\n'死',\n'讲',\n'配',\n'女',\n'黄',\n'推',\n'显',\n'谈',\n'罪',\n'神',\n'艺',\n'呢',\n'席',\n'含',\n'企',\n'望',\n'密',\n'批',\n'营',\n'项',\n'防',\n'举',\n'球',\n'英',\n'氧',\n'势',\n'告',\n'李',\n'台',\n'落',\n'木',\n'帮',\n'轮',\n'破',\n'亚',\n'师',\n'围',\n'注',\n'远',\n'字',\n'材',\n'排',\n'供',\n'河',\n'态',\n'封',\n'另',\n'施',\n'减',\n'树',\n'溶',\n'怎',\n'止',\n'案',\n'言',\n'士',\n'均',\n'武',\n'固',\n'叶',\n'鱼',\n'波',\n'视',\n'仅',\n'费',\n'紧',\n'爱',\n'左',\n'章',\n'早',\n'朝',\n'害',\n'续',\n'轻',\n'服',\n'试',\n'食',\n'充',\n'兵',\n'源',\n'判',\n'护',\n'司',\n'足',\n'某',\n'练',\n'差',\n'致',\n'板',\n'田',\n'降',\n'黑',\n'犯',\n'负',\n'击',\n'范',\n'继',\n'兴',\n'似',\n'余',\n'坚',\n'曲',\n'输',\n'修',\n'故',\n'城',\n'夫',\n'够',\n'送',\n'笔',\n'船',\n'占',\n'右',\n'财',\n'吃',\n'富',\n'春',\n'职',\n'觉',\n'汉',\n'画',\n'功',\n'巴',\n'跟',\n'虽',\n'杂',\n'飞',\n'检',\n'吸',\n'助',\n'升',\n'阳',\n'互',\n'初',\n'创',\n'抗',\n'考',\n'投',\n'坏',\n'策',\n'古',\n'径',\n'换',\n'未',\n'跑',\n'留',\n'钢',\n'曾',\n'端',\n'责',\n'站',\n'简',\n'述',\n'钱',\n'副',\n'尽',\n'帝',\n'射',\n'草',\n'冲',\n'承',\n'独',\n'令',\n'限',\n'阿',\n'宣',\n'环',\n'双',\n'请',\n'超',\n'微',\n'让',\n'控',\n'州',\n'良',\n'轴',\n'找',\n'否',\n'纪',\n'益',\n'依',\n'优',\n'顶',\n'础',\n'载',\n'倒',\n'房',\n'突',\n'坐',\n'粉',\n'敌',\n'略',\n'客',\n'袁',\n'冷',\n'胜',\n'绝',\n'析',\n'块',\n'剂',\n'测',\n'丝',\n'协',\n'诉',\n'念',\n'陈',\n'仍',\n'罗',\n'盐',\n'友',\n'洋',\n'错',\n'苦',\n'夜',\n'刑',\n'移',\n'频',\n'逐',\n'靠',\n'混',\n'母',\n'短',\n'皮',\n'终',\n'聚',\n'汽',\n'村',\n'云',\n'哪',\n'既',\n'距',\n'卫',\n'停',\n'烈',\n'央',\n'察',\n'烧',\n'迅',\n'境',\n'若',\n'印',\n'洲',\n'刻',\n'括',\n'激',\n'孔',\n'搞',\n'甚',\n'室',\n'待',\n'核',\n'校',\n'散',\n'侵',\n'吧',\n'甲',\n'游',\n'久',\n'菜',\n'味',\n'旧',\n'模',\n'湖',\n'货',\n'损',\n'预',\n'阻',\n'毫',\n'普',\n'稳',\n'乙',\n'妈',\n'植',\n'息',\n'扩',\n'银',\n'语',\n'挥',\n'酒',\n'守',\n'拿',\n'序',\n'纸',\n'医',\n'缺',\n'雨',\n'吗',\n'针',\n'刘',\n'啊',\n'急',\n'唱',\n'误',\n'训',\n'愿',\n'审',\n'附',\n'获',\n'茶',\n'鲜',\n'粮',\n'斤',\n'孩',\n'脱',\n'硫',\n'肥',\n'善',\n'龙',\n'演',\n'父',\n'渐',\n'血',\n'欢',\n'械',\n'掌',\n'歌',\n'沙',\n'刚',\n'攻',\n'谓',\n'盾',\n'讨',\n'晚',\n'粒',\n'乱',\n'燃',\n'矛',\n'乎',\n'杀',\n'药',\n'宁',\n'鲁',\n'贵',\n'钟',\n'煤',\n'读',\n'班',\n'伯',\n'香',\n'介',\n'迫',\n'句',\n'丰',\n'培',\n'握',\n'兰',\n'担',\n'弦',\n'蛋',\n'沉',\n'假',\n'穿',\n'执',\n'答',\n'乐',\n'谁',\n'顺',\n'烟',\n'缩',\n'征',\n'脸',\n'喜',\n'松',\n'脚',\n'困',\n'异',\n'免',\n'背',\n'星',\n'福',\n'买',\n'染',\n'井',\n'概',\n'慢',\n'怕',\n'磁',\n'倍',\n'祖',\n'皇',\n'促',\n'静',\n'补',\n'评',\n'翻',\n'肉',\n'践',\n'尼',\n'衣',\n'宽',\n'扬',\n'棉',\n'希',\n'伤',\n'操',\n'垂',\n'秋',\n'宜',\n'氢',\n'套',\n'督',\n'振',\n'架',\n'亮',\n'末',\n'宪',\n'庆',\n'编',\n'牛',\n'触',\n'映',\n'雷',\n'销',\n'诗',\n'座',\n'居',\n'抓',\n'裂',\n'胞',\n'呼',\n'娘',\n'景',\n'威',\n'绿',\n'晶',\n'厚',\n'盟',\n'衡',\n'鸡',\n'孙',\n'延',\n'危',\n'胶',\n'屋',\n'乡',\n'临',\n'陆',\n'顾',\n'掉',\n'呀',\n'灯',\n'岁',\n'措',\n'束',\n'耐',\n'剧',\n'玉',\n'赵',\n'跳',\n'哥',\n'季',\n'课',\n'凯',\n'胡',\n'额',\n'款',\n'绍',\n'卷',\n'齐',\n'伟',\n'蒸',\n'殖',\n'永',\n'宗',\n'苗',\n'川',\n'炉',\n'岩',\n'弱',\n'零',\n'杨',\n'奏',\n'沿',\n'露',\n'杆',\n'探',\n'滑',\n'镇',\n'饭',\n'浓',\n'航',\n'怀',\n'赶',\n'库',\n'夺',\n'伊',\n'灵',\n'税',\n'途',\n'灭',\n'赛',\n'归',\n'召',\n'鼓',\n'播',\n'盘',\n'裁',\n'险',\n'康',\n'唯',\n'录',\n'菌',\n'纯',\n'借',\n'糖',\n'盖',\n'横',\n'符',\n'私',\n'努',\n'堂',\n'域',\n'枪',\n'润',\n'幅',\n'哈',\n'竟',\n'熟',\n'虫',\n'泽',\n'脑',\n'壤',\n'碳',\n'欧',\n'遍',\n'侧',\n'寨',\n'敢',\n'彻',\n'虑',\n'斜',\n'薄',\n'庭',\n'纳',\n'弹',\n'饲',\n'伸',\n'折',\n'麦',\n'湿',\n'暗',\n'荷',\n'瓦',\n'塞',\n'床',\n'筑',\n'恶',\n'户',\n'访',\n'塔',\n'奇',\n'透',\n'梁',\n'刀',\n'旋',\n'迹',\n'卡',\n'氯',\n'遇',\n'份',\n'毒',\n'泥',\n'退',\n'洗',\n'摆',\n'灰',\n'彩',\n'卖',\n'耗',\n'夏',\n'择',\n'忙',\n'铜',\n'献',\n'硬',\n'予',\n'繁',\n'圈',\n'雪',\n'函',\n'亦',\n'抽',\n'篇',\n'阵',\n'阴',\n'丁',\n'尺',\n'追',\n'堆',\n'雄',\n'迎',\n'泛',\n'爸',\n'楼',\n'避',\n'谋',\n'吨',\n'野',\n'猪',\n'旗',\n'累',\n'偏',\n'典',\n'馆',\n'索',\n'秦',\n'脂',\n'潮',\n'爷',\n'豆',\n'忽',\n'托',\n'惊',\n'塑',\n'遗',\n'愈',\n'朱',\n'替',\n'纤',\n'粗',\n'倾',\n'尚',\n'痛',\n'楚',\n'谢',\n'奋',\n'购',\n'磨',\n'君',\n'池',\n'旁',\n'碎',\n'骨',\n'监',\n'捕',\n'弟',\n'暴',\n'割',\n'贯',\n'殊',\n'释',\n'词',\n'亡',\n'壁',\n'顿',\n'宝',\n'午',\n'尘',\n'闻',\n'揭',\n'炮',\n'残',\n'冬',\n'桥',\n'妇',\n'警',\n'综',\n'招',\n'吴',\n'付',\n'浮',\n'遭',\n'徐',\n'您',\n'摇',\n'谷',\n'赞',\n'箱',\n'隔',\n'订',\n'男',\n'吹',\n'园',\n'纷',\n'唐',\n'败',\n'宋',\n'玻',\n'巨',\n'耕',\n'坦',\n'荣',\n'闭',\n'湾',\n'键',\n'凡',\n'驻',\n'锅',\n'救',\n'恩',\n'剥',\n'凝',\n'碱',\n'齿',\n'截',\n'炼',\n'麻',\n'纺',\n'禁',\n'废',\n'盛',\n'版',\n'缓',\n'净',\n'睛',\n'昌',\n'婚',\n'涉',\n'筒',\n'嘴',\n'插',\n'岸',\n'朗',\n'庄',\n'街',\n'藏',\n'姑',\n'贸',\n'腐',\n'奴',\n'啦',\n'惯',\n'乘',\n'伙',\n'恢',\n'匀',\n'纱',\n'扎',\n'辩',\n'耳',\n'彪',\n'臣',\n'亿',\n'璃',\n'抵',\n'脉',\n'秀',\n'萨',\n'俄',\n'网',\n'舞',\n'店',\n'喷',\n'纵',\n'寸',\n'汗',\n'挂',\n'洪',\n'贺',\n'闪',\n'柬',\n'爆',\n'烯',\n'津',\n'稻',\n'墙',\n'软',\n'勇',\n'像',\n'滚',\n'厘',\n'蒙',\n'芳',\n'肯',\n'坡',\n'柱',\n'荡',\n'腿',\n'仪',\n'旅',\n'尾',\n'轧',\n'冰',\n'贡',\n'登',\n'黎',\n'削',\n'钻',\n'勒',\n'逃',\n'障',\n'氨',\n'郭',\n'峰',\n'币',\n'港',\n'伏',\n'轨',\n'亩',\n'毕',\n'擦',\n'莫',\n'刺',\n'浪',\n'秘',\n'援',\n'株',\n'健',\n'售',\n'股',\n'岛',\n'甘',\n'泡',\n'睡',\n'童',\n'铸',\n'汤',\n'阀',\n'休',\n'汇',\n'舍',\n'牧',\n'绕',\n'炸',\n'哲',\n'磷',\n'绩',\n'朋',\n'淡',\n'尖',\n'启',\n'陷',\n'柴',\n'呈',\n'徒',\n'颜',\n'泪',\n'稍',\n'忘',\n'泵',\n'蓝',\n'拖',\n'洞',\n'授',\n'镜',\n'辛',\n'壮',\n'锋',\n'贫',\n'虚',\n'弯',\n'摩',\n'泰',\n'幼',\n'廷',\n'尊',\n'窗',\n'纲',\n'弄',\n'隶',\n'疑',\n'氏',\n'宫',\n'姐',\n'震',\n'瑞',\n'怪',\n'尤',\n'琴',\n'循',\n'描',\n'膜',\n'违',\n'夹',\n'腰',\n'缘',\n'珠',\n'穷',\n'森',\n'枝',\n'竹',\n'沟',\n'催',\n'绳',\n'忆',\n'邦',\n'剩',\n'幸',\n'浆',\n'栏',\n'拥',\n'牙',\n'贮',\n'礼',\n'滤',\n'钠',\n'纹',\n'罢',\n'拍',\n'咱',\n'喊',\n'袖',\n'埃',\n'勤',\n'罚',\n'焦',\n'潜',\n'伍',\n'墨',\n'欲',\n'缝',\n'姓',\n'刊',\n'饱',\n'仿',\n'奖',\n'铝',\n'鬼',\n'丽',\n'跨',\n'默',\n'挖',\n'链',\n'扫',\n'喝',\n'袋',\n'炭',\n'污',\n'幕',\n'诸',\n'弧',\n'励',\n'梅',\n'奶',\n'洁',\n'灾',\n'舟',\n'鉴',\n'苯',\n'讼',\n'抱',\n'毁',\n'懂',\n'寒',\n'智',\n'埔',\n'寄',\n'届',\n'跃',\n'渡',\n'挑',\n'丹',\n'艰',\n'贝',\n'碰',\n'拔',\n'爹',\n'戴',\n'码',\n'梦',\n'芽',\n'熔',\n'赤',\n'渔',\n'哭',\n'敬',\n'颗',\n'奔',\n'铅',\n'仲',\n'虎',\n'稀',\n'妹',\n'乏',\n'珍',\n'申',\n'桌',\n'遵',\n'允',\n'隆',\n'螺',\n'仓',\n'魏',\n'锐',\n'晓',\n'氮',\n'兼',\n'隐',\n'碍',\n'赫',\n'拨',\n'忠',\n'肃',\n'缸',\n'牵',\n'抢',\n'博',\n'巧',\n'壳',\n'兄',\n'杜',\n'讯',\n'诚',\n'碧',\n'祥',\n'柯',\n'页',\n'巡',\n'矩',\n'悲',\n'灌',\n'龄',\n'伦',\n'票',\n'寻',\n'桂',\n'铺',\n'圣',\n'恐',\n'恰',\n'郑',\n'趣',\n'抬',\n'荒',\n'腾',\n'贴',\n'柔',\n'滴',\n'猛',\n'阔',\n'辆',\n'妻',\n'填',\n'撤',\n'储',\n'签',\n'闹',\n'扰',\n'紫',\n'砂',\n'递',\n'戏',\n'吊',\n'陶',\n'伐',\n'喂',\n'疗',\n'瓶',\n'婆',\n'抚',\n'臂',\n'摸',\n'忍',\n'虾',\n'蜡',\n'邻',\n'胸',\n'巩',\n'挤',\n'偶',\n'弃',\n'槽',\n'劲',\n'乳',\n'邓',\n'吉',\n'仁',\n'烂',\n'砖',\n'租',\n'乌',\n'舰',\n'伴',\n'瓜',\n'浅',\n'丙',\n'暂',\n'燥',\n'橡',\n'柳',\n'迷',\n'暖',\n'牌',\n'秧',\n'胆',\n'详',\n'簧',\n'踏',\n'瓷',\n'谱',\n'呆',\n'宾',\n'糊',\n'洛',\n'辉',\n'愤',\n'竞',\n'隙',\n'怒',\n'粘',\n'乃',\n'绪',\n'肩',\n'籍',\n'敏',\n'涂',\n'熙',\n'皆',\n'侦',\n'悬',\n'掘',\n'享',\n'纠',\n'醒',\n'狂',\n'锁',\n'淀',\n'恨',\n'牲',\n'霸',\n'爬',\n'赏',\n'逆',\n'玩',\n'陵',\n'祝',\n'秒',\n'浙',\n'貌',\n'役',\n'彼',\n'悉',\n'鸭',\n'趋',\n'凤',\n'晨',\n'畜',\n'辈',\n'秩',\n'卵',\n'署',\n'梯',\n'炎',\n'滩',\n'棋',\n'驱',\n'筛',\n'峡',\n'冒',\n'啥',\n'寿',\n'译',\n'浸',\n'泉',\n'帽',\n'迟',\n'硅',\n'疆',\n'贷',\n'漏',\n'稿',\n'冠',\n'嫩',\n'胁',\n'芯',\n'牢',\n'叛',\n'蚀',\n'奥',\n'鸣',\n'岭',\n'羊',\n'凭',\n'串',\n'塘',\n'绘',\n'酵',\n'融',\n'盆',\n'锡',\n'庙',\n'筹',\n'冻',\n'辅',\n'摄',\n'袭',\n'筋',\n'拒',\n'僚',\n'旱',\n'钾',\n'鸟',\n'漆',\n'沈',\n'眉',\n'疏',\n'添',\n'棒',\n'穗',\n'硝',\n'韩',\n'逼',\n'扭',\n'侨',\n'凉',\n'挺',\n'碗',\n'栽',\n'炒',\n'杯',\n'患',\n'馏',\n'劝',\n'豪',\n'辽',\n'勃',\n'鸿',\n'旦',\n'吏',\n'拜',\n'狗',\n'埋',\n'辊',\n'掩',\n'饮',\n'搬',\n'骂',\n'辞',\n'勾',\n'扣',\n'估',\n'蒋',\n'绒',\n'雾',\n'丈',\n'朵',\n'姆',\n'拟',\n'宇',\n'辑',\n'陕',\n'雕',\n'偿',\n'蓄',\n'崇',\n'剪',\n'倡',\n'厅',\n'咬',\n'驶',\n'薯',\n'刷',\n'斥',\n'番',\n'赋',\n'奉',\n'佛',\n'浇',\n'漫',\n'曼',\n'扇',\n'钙',\n'桃',\n'扶',\n'仔',\n'返',\n'俗',\n'亏',\n'腔',\n'鞋',\n'棱',\n'覆',\n'框',\n'悄',\n'叔',\n'撞',\n'骗',\n'勘',\n'旺',\n'沸',\n'孤',\n'吐',\n'孟',\n'渠',\n'屈',\n'疾',\n'妙',\n'惜',\n'仰',\n'狠',\n'胀',\n'谐',\n'抛',\n'霉',\n'桑',\n'岗',\n'嘛',\n'衰',\n'盗',\n'渗',\n'脏',\n'赖',\n'涌',\n'甜',\n'曹',\n'阅',\n'肌',\n'哩',\n'厉',\n'烃',\n'纬',\n'毅',\n'昨',\n'伪',\n'症',\n'煮',\n'叹',\n'钉',\n'搭',\n'茎',\n'笼',\n'酷',\n'偷',\n'弓',\n'锥',\n'恒',\n'杰',\n'坑',\n'鼻',\n'翼',\n'纶',\n'叙',\n'狱',\n'逮',\n'罐',\n'络',\n'棚',\n'抑',\n'膨',\n'蔬',\n'寺',\n'骤',\n'穆',\n'冶',\n'枯',\n'册',\n'尸',\n'凸',\n'绅',\n'坯',\n'牺',\n'焰',\n'轰',\n'欣',\n'晋',\n'瘦',\n'御',\n'锭',\n'锦',\n'丧',\n'旬',\n'锻',\n'垄',\n'搜',\n'扑',\n'邀',\n'亭',\n'酯',\n'迈',\n'舒',\n'脆',\n'酶',\n'闲',\n'忧',\n'酚',\n'顽',\n'羽',\n'涨',\n'卸',\n'仗',\n'陪',\n'辟',\n'惩',\n'杭',\n'姚',\n'肚',\n'捉',\n'飘',\n'漂',\n'昆',\n'欺',\n'吾',\n'郎',\n'烷',\n'汁',\n'呵',\n'饰',\n'萧',\n'雅',\n'邮',\n'迁',\n'燕',\n'撒',\n'姻',\n'赴',\n'宴',\n'烦',\n'债',\n'帐',\n'斑',\n'铃',\n'旨',\n'醇',\n'董',\n'饼',\n'雏',\n'姿',\n'拌',\n'傅',\n'腹',\n'妥',\n'揉',\n'贤',\n'拆',\n'歪',\n'葡',\n'胺',\n'丢',\n'浩',\n'徽',\n'昂',\n'垫',\n'挡',\n'览',\n'贪',\n'慰',\n'缴',\n'汪',\n'慌',\n'冯',\n'诺',\n'姜',\n'谊',\n'凶',\n'劣',\n'诬',\n'耀',\n'昏',\n'躺',\n'盈',\n'骑',\n'乔',\n'溪',\n'丛',\n'卢',\n'抹',\n'闷',\n'咨',\n'刮',\n'驾',\n'缆',\n'悟',\n'摘',\n'铒',\n'掷',\n'颇',\n'幻',\n'柄',\n'惠',\n'惨',\n'佳',\n'仇',\n'腊',\n'窝',\n'涤',\n'剑',\n'瞧',\n'堡',\n'泼',\n'葱',\n'罩',\n'霍',\n'捞',\n'胎',\n'苍',\n'滨',\n'俩',\n'捅',\n'湘',\n'砍',\n'霞',\n'邵',\n'萄',\n'疯',\n'淮',\n'遂',\n'熊',\n'粪',\n'烘',\n'宿',\n'档',\n'戈',\n'驳',\n'嫂',\n'裕',\n'徙',\n'箭',\n'捐',\n'肠',\n'撑',\n'晒',\n'辨',\n'殿',\n'莲',\n'摊',\n'搅',\n'酱',\n'屏',\n'疫',\n'哀',\n'蔡',\n'堵',\n'沫',\n'皱',\n'畅',\n'叠',\n'阁',\n'莱',\n'敲',\n'辖',\n'钩',\n'痕',\n'坝',\n'巷',\n'饿',\n'祸',\n'丘',\n'玄',\n'溜',\n'曰',\n'逻',\n'彭',\n'尝',\n'卿',\n'妨',\n'艇',\n'吞',\n'韦',\n'怨',\n'矮',\n'歇'\n]\n"
  },
  {
    "path": "lbry/wallet/words/english.py",
    "content": "words = [\n'abandon',\n'ability',\n'able',\n'about',\n'above',\n'absent',\n'absorb',\n'abstract',\n'absurd',\n'abuse',\n'access',\n'accident',\n'account',\n'accuse',\n'achieve',\n'acid',\n'acoustic',\n'acquire',\n'across',\n'act',\n'action',\n'actor',\n'actress',\n'actual',\n'adapt',\n'add',\n'addict',\n'address',\n'adjust',\n'admit',\n'adult',\n'advance',\n'advice',\n'aerobic',\n'affair',\n'afford',\n'afraid',\n'again',\n'age',\n'agent',\n'agree',\n'ahead',\n'aim',\n'air',\n'airport',\n'aisle',\n'alarm',\n'album',\n'alcohol',\n'alert',\n'alien',\n'all',\n'alley',\n'allow',\n'almost',\n'alone',\n'alpha',\n'already',\n'also',\n'alter',\n'always',\n'amateur',\n'amazing',\n'among',\n'amount',\n'amused',\n'analyst',\n'anchor',\n'ancient',\n'anger',\n'angle',\n'angry',\n'animal',\n'ankle',\n'announce',\n'annual',\n'another',\n'answer',\n'antenna',\n'antique',\n'anxiety',\n'any',\n'apart',\n'apology',\n'appear',\n'apple',\n'approve',\n'april',\n'arch',\n'arctic',\n'area',\n'arena',\n'argue',\n'arm',\n'armed',\n'armor',\n'army',\n'around',\n'arrange',\n'arrest',\n'arrive',\n'arrow',\n'art',\n'artefact',\n'artist',\n'artwork',\n'ask',\n'aspect',\n'assault',\n'asset',\n'assist',\n'assume',\n'asthma',\n'athlete',\n'atom',\n'attack',\n'attend',\n'attitude',\n'attract',\n'auction',\n'audit',\n'august',\n'aunt',\n'author',\n'auto',\n'autumn',\n'average',\n'avocado',\n'avoid',\n'awake',\n'aware',\n'away',\n'awesome',\n'awful',\n'awkward',\n'axis',\n'baby',\n'bachelor',\n'bacon',\n'badge',\n'bag',\n'balance',\n'balcony',\n'ball',\n'bamboo',\n'banana',\n'banner',\n'bar',\n'barely',\n'bargain',\n'barrel',\n'base',\n'basic',\n'basket',\n'battle',\n'beach',\n'bean',\n'beauty',\n'because',\n'become',\n'beef',\n'before',\n'begin',\n'behave',\n'behind',\n'believe',\n'below',\n'belt',\n'bench',\n'benefit',\n'best',\n'betray',\n'better',\n'between',\n'beyond',\n'bicycle',\n'bid',\n'bike',\n'bind',\n'biology',\n'bird',\n'birth',\n'bitter',\n'black',\n'blade',\n'blame',\n'blanket',\n'blast',\n'bleak',\n'bless',\n'blind',\n'blood',\n'blossom',\n'blouse',\n'blue',\n'blur',\n'blush',\n'board',\n'boat',\n'body',\n'boil',\n'bomb',\n'bone',\n'bonus',\n'book',\n'boost',\n'border',\n'boring',\n'borrow',\n'boss',\n'bottom',\n'bounce',\n'box',\n'boy',\n'bracket',\n'brain',\n'brand',\n'brass',\n'brave',\n'bread',\n'breeze',\n'brick',\n'bridge',\n'brief',\n'bright',\n'bring',\n'brisk',\n'broccoli',\n'broken',\n'bronze',\n'broom',\n'brother',\n'brown',\n'brush',\n'bubble',\n'buddy',\n'budget',\n'buffalo',\n'build',\n'bulb',\n'bulk',\n'bullet',\n'bundle',\n'bunker',\n'burden',\n'burger',\n'burst',\n'bus',\n'business',\n'busy',\n'butter',\n'buyer',\n'buzz',\n'cabbage',\n'cabin',\n'cable',\n'cactus',\n'cage',\n'cake',\n'call',\n'calm',\n'camera',\n'camp',\n'can',\n'canal',\n'cancel',\n'candy',\n'cannon',\n'canoe',\n'canvas',\n'canyon',\n'capable',\n'capital',\n'captain',\n'car',\n'carbon',\n'card',\n'cargo',\n'carpet',\n'carry',\n'cart',\n'case',\n'cash',\n'casino',\n'castle',\n'casual',\n'cat',\n'catalog',\n'catch',\n'category',\n'cattle',\n'caught',\n'cause',\n'caution',\n'cave',\n'ceiling',\n'celery',\n'cement',\n'census',\n'century',\n'cereal',\n'certain',\n'chair',\n'chalk',\n'champion',\n'change',\n'chaos',\n'chapter',\n'charge',\n'chase',\n'chat',\n'cheap',\n'check',\n'cheese',\n'chef',\n'cherry',\n'chest',\n'chicken',\n'chief',\n'child',\n'chimney',\n'choice',\n'choose',\n'chronic',\n'chuckle',\n'chunk',\n'churn',\n'cigar',\n'cinnamon',\n'circle',\n'citizen',\n'city',\n'civil',\n'claim',\n'clap',\n'clarify',\n'claw',\n'clay',\n'clean',\n'clerk',\n'clever',\n'click',\n'client',\n'cliff',\n'climb',\n'clinic',\n'clip',\n'clock',\n'clog',\n'close',\n'cloth',\n'cloud',\n'clown',\n'club',\n'clump',\n'cluster',\n'clutch',\n'coach',\n'coast',\n'coconut',\n'code',\n'coffee',\n'coil',\n'coin',\n'collect',\n'color',\n'column',\n'combine',\n'come',\n'comfort',\n'comic',\n'common',\n'company',\n'concert',\n'conduct',\n'confirm',\n'congress',\n'connect',\n'consider',\n'control',\n'convince',\n'cook',\n'cool',\n'copper',\n'copy',\n'coral',\n'core',\n'corn',\n'correct',\n'cost',\n'cotton',\n'couch',\n'country',\n'couple',\n'course',\n'cousin',\n'cover',\n'coyote',\n'crack',\n'cradle',\n'craft',\n'cram',\n'crane',\n'crash',\n'crater',\n'crawl',\n'crazy',\n'cream',\n'credit',\n'creek',\n'crew',\n'cricket',\n'crime',\n'crisp',\n'critic',\n'crop',\n'cross',\n'crouch',\n'crowd',\n'crucial',\n'cruel',\n'cruise',\n'crumble',\n'crunch',\n'crush',\n'cry',\n'crystal',\n'cube',\n'culture',\n'cup',\n'cupboard',\n'curious',\n'current',\n'curtain',\n'curve',\n'cushion',\n'custom',\n'cute',\n'cycle',\n'dad',\n'damage',\n'damp',\n'dance',\n'danger',\n'daring',\n'dash',\n'daughter',\n'dawn',\n'day',\n'deal',\n'debate',\n'debris',\n'decade',\n'december',\n'decide',\n'decline',\n'decorate',\n'decrease',\n'deer',\n'defense',\n'define',\n'defy',\n'degree',\n'delay',\n'deliver',\n'demand',\n'demise',\n'denial',\n'dentist',\n'deny',\n'depart',\n'depend',\n'deposit',\n'depth',\n'deputy',\n'derive',\n'describe',\n'desert',\n'design',\n'desk',\n'despair',\n'destroy',\n'detail',\n'detect',\n'develop',\n'device',\n'devote',\n'diagram',\n'dial',\n'diamond',\n'diary',\n'dice',\n'diesel',\n'diet',\n'differ',\n'digital',\n'dignity',\n'dilemma',\n'dinner',\n'dinosaur',\n'direct',\n'dirt',\n'disagree',\n'discover',\n'disease',\n'dish',\n'dismiss',\n'disorder',\n'display',\n'distance',\n'divert',\n'divide',\n'divorce',\n'dizzy',\n'doctor',\n'document',\n'dog',\n'doll',\n'dolphin',\n'domain',\n'donate',\n'donkey',\n'donor',\n'door',\n'dose',\n'double',\n'dove',\n'draft',\n'dragon',\n'drama',\n'drastic',\n'draw',\n'dream',\n'dress',\n'drift',\n'drill',\n'drink',\n'drip',\n'drive',\n'drop',\n'drum',\n'dry',\n'duck',\n'dumb',\n'dune',\n'during',\n'dust',\n'dutch',\n'duty',\n'dwarf',\n'dynamic',\n'eager',\n'eagle',\n'early',\n'earn',\n'earth',\n'easily',\n'east',\n'easy',\n'echo',\n'ecology',\n'economy',\n'edge',\n'edit',\n'educate',\n'effort',\n'egg',\n'eight',\n'either',\n'elbow',\n'elder',\n'electric',\n'elegant',\n'element',\n'elephant',\n'elevator',\n'elite',\n'else',\n'embark',\n'embody',\n'embrace',\n'emerge',\n'emotion',\n'employ',\n'empower',\n'empty',\n'enable',\n'enact',\n'end',\n'endless',\n'endorse',\n'enemy',\n'energy',\n'enforce',\n'engage',\n'engine',\n'enhance',\n'enjoy',\n'enlist',\n'enough',\n'enrich',\n'enroll',\n'ensure',\n'enter',\n'entire',\n'entry',\n'envelope',\n'episode',\n'equal',\n'equip',\n'era',\n'erase',\n'erode',\n'erosion',\n'error',\n'erupt',\n'escape',\n'essay',\n'essence',\n'estate',\n'eternal',\n'ethics',\n'evidence',\n'evil',\n'evoke',\n'evolve',\n'exact',\n'example',\n'excess',\n'exchange',\n'excite',\n'exclude',\n'excuse',\n'execute',\n'exercise',\n'exhaust',\n'exhibit',\n'exile',\n'exist',\n'exit',\n'exotic',\n'expand',\n'expect',\n'expire',\n'explain',\n'expose',\n'express',\n'extend',\n'extra',\n'eye',\n'eyebrow',\n'fabric',\n'face',\n'faculty',\n'fade',\n'faint',\n'faith',\n'fall',\n'false',\n'fame',\n'family',\n'famous',\n'fan',\n'fancy',\n'fantasy',\n'farm',\n'fashion',\n'fat',\n'fatal',\n'father',\n'fatigue',\n'fault',\n'favorite',\n'feature',\n'february',\n'federal',\n'fee',\n'feed',\n'feel',\n'female',\n'fence',\n'festival',\n'fetch',\n'fever',\n'few',\n'fiber',\n'fiction',\n'field',\n'figure',\n'file',\n'film',\n'filter',\n'final',\n'find',\n'fine',\n'finger',\n'finish',\n'fire',\n'firm',\n'first',\n'fiscal',\n'fish',\n'fit',\n'fitness',\n'fix',\n'flag',\n'flame',\n'flash',\n'flat',\n'flavor',\n'flee',\n'flight',\n'flip',\n'float',\n'flock',\n'floor',\n'flower',\n'fluid',\n'flush',\n'fly',\n'foam',\n'focus',\n'fog',\n'foil',\n'fold',\n'follow',\n'food',\n'foot',\n'force',\n'forest',\n'forget',\n'fork',\n'fortune',\n'forum',\n'forward',\n'fossil',\n'foster',\n'found',\n'fox',\n'fragile',\n'frame',\n'frequent',\n'fresh',\n'friend',\n'fringe',\n'frog',\n'front',\n'frost',\n'frown',\n'frozen',\n'fruit',\n'fuel',\n'fun',\n'funny',\n'furnace',\n'fury',\n'future',\n'gadget',\n'gain',\n'galaxy',\n'gallery',\n'game',\n'gap',\n'garage',\n'garbage',\n'garden',\n'garlic',\n'garment',\n'gas',\n'gasp',\n'gate',\n'gather',\n'gauge',\n'gaze',\n'general',\n'genius',\n'genre',\n'gentle',\n'genuine',\n'gesture',\n'ghost',\n'giant',\n'gift',\n'giggle',\n'ginger',\n'giraffe',\n'girl',\n'give',\n'glad',\n'glance',\n'glare',\n'glass',\n'glide',\n'glimpse',\n'globe',\n'gloom',\n'glory',\n'glove',\n'glow',\n'glue',\n'goat',\n'goddess',\n'gold',\n'good',\n'goose',\n'gorilla',\n'gospel',\n'gossip',\n'govern',\n'gown',\n'grab',\n'grace',\n'grain',\n'grant',\n'grape',\n'grass',\n'gravity',\n'great',\n'green',\n'grid',\n'grief',\n'grit',\n'grocery',\n'group',\n'grow',\n'grunt',\n'guard',\n'guess',\n'guide',\n'guilt',\n'guitar',\n'gun',\n'gym',\n'habit',\n'hair',\n'half',\n'hammer',\n'hamster',\n'hand',\n'happy',\n'harbor',\n'hard',\n'harsh',\n'harvest',\n'hat',\n'have',\n'hawk',\n'hazard',\n'head',\n'health',\n'heart',\n'heavy',\n'hedgehog',\n'height',\n'hello',\n'helmet',\n'help',\n'hen',\n'hero',\n'hidden',\n'high',\n'hill',\n'hint',\n'hip',\n'hire',\n'history',\n'hobby',\n'hockey',\n'hold',\n'hole',\n'holiday',\n'hollow',\n'home',\n'honey',\n'hood',\n'hope',\n'horn',\n'horror',\n'horse',\n'hospital',\n'host',\n'hotel',\n'hour',\n'hover',\n'hub',\n'huge',\n'human',\n'humble',\n'humor',\n'hundred',\n'hungry',\n'hunt',\n'hurdle',\n'hurry',\n'hurt',\n'husband',\n'hybrid',\n'ice',\n'icon',\n'idea',\n'identify',\n'idle',\n'ignore',\n'ill',\n'illegal',\n'illness',\n'image',\n'imitate',\n'immense',\n'immune',\n'impact',\n'impose',\n'improve',\n'impulse',\n'inch',\n'include',\n'income',\n'increase',\n'index',\n'indicate',\n'indoor',\n'industry',\n'infant',\n'inflict',\n'inform',\n'inhale',\n'inherit',\n'initial',\n'inject',\n'injury',\n'inmate',\n'inner',\n'innocent',\n'input',\n'inquiry',\n'insane',\n'insect',\n'inside',\n'inspire',\n'install',\n'intact',\n'interest',\n'into',\n'invest',\n'invite',\n'involve',\n'iron',\n'island',\n'isolate',\n'issue',\n'item',\n'ivory',\n'jacket',\n'jaguar',\n'jar',\n'jazz',\n'jealous',\n'jeans',\n'jelly',\n'jewel',\n'job',\n'join',\n'joke',\n'journey',\n'joy',\n'judge',\n'juice',\n'jump',\n'jungle',\n'junior',\n'junk',\n'just',\n'kangaroo',\n'keen',\n'keep',\n'ketchup',\n'key',\n'kick',\n'kid',\n'kidney',\n'kind',\n'kingdom',\n'kiss',\n'kit',\n'kitchen',\n'kite',\n'kitten',\n'kiwi',\n'knee',\n'knife',\n'knock',\n'know',\n'lab',\n'label',\n'labor',\n'ladder',\n'lady',\n'lake',\n'lamp',\n'language',\n'laptop',\n'large',\n'later',\n'latin',\n'laugh',\n'laundry',\n'lava',\n'law',\n'lawn',\n'lawsuit',\n'layer',\n'lazy',\n'leader',\n'leaf',\n'learn',\n'leave',\n'lecture',\n'left',\n'leg',\n'legal',\n'legend',\n'leisure',\n'lemon',\n'lend',\n'length',\n'lens',\n'leopard',\n'lesson',\n'letter',\n'level',\n'liar',\n'liberty',\n'library',\n'license',\n'life',\n'lift',\n'light',\n'like',\n'limb',\n'limit',\n'link',\n'lion',\n'liquid',\n'list',\n'little',\n'live',\n'lizard',\n'load',\n'loan',\n'lobster',\n'local',\n'lock',\n'logic',\n'lonely',\n'long',\n'loop',\n'lottery',\n'loud',\n'lounge',\n'love',\n'loyal',\n'lucky',\n'luggage',\n'lumber',\n'lunar',\n'lunch',\n'luxury',\n'lyrics',\n'machine',\n'mad',\n'magic',\n'magnet',\n'maid',\n'mail',\n'main',\n'major',\n'make',\n'mammal',\n'man',\n'manage',\n'mandate',\n'mango',\n'mansion',\n'manual',\n'maple',\n'marble',\n'march',\n'margin',\n'marine',\n'market',\n'marriage',\n'mask',\n'mass',\n'master',\n'match',\n'material',\n'math',\n'matrix',\n'matter',\n'maximum',\n'maze',\n'meadow',\n'mean',\n'measure',\n'meat',\n'mechanic',\n'medal',\n'media',\n'melody',\n'melt',\n'member',\n'memory',\n'mention',\n'menu',\n'mercy',\n'merge',\n'merit',\n'merry',\n'mesh',\n'message',\n'metal',\n'method',\n'middle',\n'midnight',\n'milk',\n'million',\n'mimic',\n'mind',\n'minimum',\n'minor',\n'minute',\n'miracle',\n'mirror',\n'misery',\n'miss',\n'mistake',\n'mix',\n'mixed',\n'mixture',\n'mobile',\n'model',\n'modify',\n'mom',\n'moment',\n'monitor',\n'monkey',\n'monster',\n'month',\n'moon',\n'moral',\n'more',\n'morning',\n'mosquito',\n'mother',\n'motion',\n'motor',\n'mountain',\n'mouse',\n'move',\n'movie',\n'much',\n'muffin',\n'mule',\n'multiply',\n'muscle',\n'museum',\n'mushroom',\n'music',\n'must',\n'mutual',\n'myself',\n'mystery',\n'myth',\n'naive',\n'name',\n'napkin',\n'narrow',\n'nasty',\n'nation',\n'nature',\n'near',\n'neck',\n'need',\n'negative',\n'neglect',\n'neither',\n'nephew',\n'nerve',\n'nest',\n'net',\n'network',\n'neutral',\n'never',\n'news',\n'next',\n'nice',\n'night',\n'noble',\n'noise',\n'nominee',\n'noodle',\n'normal',\n'north',\n'nose',\n'notable',\n'note',\n'nothing',\n'notice',\n'novel',\n'now',\n'nuclear',\n'number',\n'nurse',\n'nut',\n'oak',\n'obey',\n'object',\n'oblige',\n'obscure',\n'observe',\n'obtain',\n'obvious',\n'occur',\n'ocean',\n'october',\n'odor',\n'off',\n'offer',\n'office',\n'often',\n'oil',\n'okay',\n'old',\n'olive',\n'olympic',\n'omit',\n'once',\n'one',\n'onion',\n'online',\n'only',\n'open',\n'opera',\n'opinion',\n'oppose',\n'option',\n'orange',\n'orbit',\n'orchard',\n'order',\n'ordinary',\n'organ',\n'orient',\n'original',\n'orphan',\n'ostrich',\n'other',\n'outdoor',\n'outer',\n'output',\n'outside',\n'oval',\n'oven',\n'over',\n'own',\n'owner',\n'oxygen',\n'oyster',\n'ozone',\n'pact',\n'paddle',\n'page',\n'pair',\n'palace',\n'palm',\n'panda',\n'panel',\n'panic',\n'panther',\n'paper',\n'parade',\n'parent',\n'park',\n'parrot',\n'party',\n'pass',\n'patch',\n'path',\n'patient',\n'patrol',\n'pattern',\n'pause',\n'pave',\n'payment',\n'peace',\n'peanut',\n'pear',\n'peasant',\n'pelican',\n'pen',\n'penalty',\n'pencil',\n'people',\n'pepper',\n'perfect',\n'permit',\n'person',\n'pet',\n'phone',\n'photo',\n'phrase',\n'physical',\n'piano',\n'picnic',\n'picture',\n'piece',\n'pig',\n'pigeon',\n'pill',\n'pilot',\n'pink',\n'pioneer',\n'pipe',\n'pistol',\n'pitch',\n'pizza',\n'place',\n'planet',\n'plastic',\n'plate',\n'play',\n'please',\n'pledge',\n'pluck',\n'plug',\n'plunge',\n'poem',\n'poet',\n'point',\n'polar',\n'pole',\n'police',\n'pond',\n'pony',\n'pool',\n'popular',\n'portion',\n'position',\n'possible',\n'post',\n'potato',\n'pottery',\n'poverty',\n'powder',\n'power',\n'practice',\n'praise',\n'predict',\n'prefer',\n'prepare',\n'present',\n'pretty',\n'prevent',\n'price',\n'pride',\n'primary',\n'print',\n'priority',\n'prison',\n'private',\n'prize',\n'problem',\n'process',\n'produce',\n'profit',\n'program',\n'project',\n'promote',\n'proof',\n'property',\n'prosper',\n'protect',\n'proud',\n'provide',\n'public',\n'pudding',\n'pull',\n'pulp',\n'pulse',\n'pumpkin',\n'punch',\n'pupil',\n'puppy',\n'purchase',\n'purity',\n'purpose',\n'purse',\n'push',\n'put',\n'puzzle',\n'pyramid',\n'quality',\n'quantum',\n'quarter',\n'question',\n'quick',\n'quit',\n'quiz',\n'quote',\n'rabbit',\n'raccoon',\n'race',\n'rack',\n'radar',\n'radio',\n'rail',\n'rain',\n'raise',\n'rally',\n'ramp',\n'ranch',\n'random',\n'range',\n'rapid',\n'rare',\n'rate',\n'rather',\n'raven',\n'raw',\n'razor',\n'ready',\n'real',\n'reason',\n'rebel',\n'rebuild',\n'recall',\n'receive',\n'recipe',\n'record',\n'recycle',\n'reduce',\n'reflect',\n'reform',\n'refuse',\n'region',\n'regret',\n'regular',\n'reject',\n'relax',\n'release',\n'relief',\n'rely',\n'remain',\n'remember',\n'remind',\n'remove',\n'render',\n'renew',\n'rent',\n'reopen',\n'repair',\n'repeat',\n'replace',\n'report',\n'require',\n'rescue',\n'resemble',\n'resist',\n'resource',\n'response',\n'result',\n'retire',\n'retreat',\n'return',\n'reunion',\n'reveal',\n'review',\n'reward',\n'rhythm',\n'rib',\n'ribbon',\n'rice',\n'rich',\n'ride',\n'ridge',\n'rifle',\n'right',\n'rigid',\n'ring',\n'riot',\n'ripple',\n'risk',\n'ritual',\n'rival',\n'river',\n'road',\n'roast',\n'robot',\n'robust',\n'rocket',\n'romance',\n'roof',\n'rookie',\n'room',\n'rose',\n'rotate',\n'rough',\n'round',\n'route',\n'royal',\n'rubber',\n'rude',\n'rug',\n'rule',\n'run',\n'runway',\n'rural',\n'sad',\n'saddle',\n'sadness',\n'safe',\n'sail',\n'salad',\n'salmon',\n'salon',\n'salt',\n'salute',\n'same',\n'sample',\n'sand',\n'satisfy',\n'satoshi',\n'sauce',\n'sausage',\n'save',\n'say',\n'scale',\n'scan',\n'scare',\n'scatter',\n'scene',\n'scheme',\n'school',\n'science',\n'scissors',\n'scorpion',\n'scout',\n'scrap',\n'screen',\n'script',\n'scrub',\n'sea',\n'search',\n'season',\n'seat',\n'second',\n'secret',\n'section',\n'security',\n'seed',\n'seek',\n'segment',\n'select',\n'sell',\n'seminar',\n'senior',\n'sense',\n'sentence',\n'series',\n'service',\n'session',\n'settle',\n'setup',\n'seven',\n'shadow',\n'shaft',\n'shallow',\n'share',\n'shed',\n'shell',\n'sheriff',\n'shield',\n'shift',\n'shine',\n'ship',\n'shiver',\n'shock',\n'shoe',\n'shoot',\n'shop',\n'short',\n'shoulder',\n'shove',\n'shrimp',\n'shrug',\n'shuffle',\n'shy',\n'sibling',\n'sick',\n'side',\n'siege',\n'sight',\n'sign',\n'silent',\n'silk',\n'silly',\n'silver',\n'similar',\n'simple',\n'since',\n'sing',\n'siren',\n'sister',\n'situate',\n'six',\n'size',\n'skate',\n'sketch',\n'ski',\n'skill',\n'skin',\n'skirt',\n'skull',\n'slab',\n'slam',\n'sleep',\n'slender',\n'slice',\n'slide',\n'slight',\n'slim',\n'slogan',\n'slot',\n'slow',\n'slush',\n'small',\n'smart',\n'smile',\n'smoke',\n'smooth',\n'snack',\n'snake',\n'snap',\n'sniff',\n'snow',\n'soap',\n'soccer',\n'social',\n'sock',\n'soda',\n'soft',\n'solar',\n'soldier',\n'solid',\n'solution',\n'solve',\n'someone',\n'song',\n'soon',\n'sorry',\n'sort',\n'soul',\n'sound',\n'soup',\n'source',\n'south',\n'space',\n'spare',\n'spatial',\n'spawn',\n'speak',\n'special',\n'speed',\n'spell',\n'spend',\n'sphere',\n'spice',\n'spider',\n'spike',\n'spin',\n'spirit',\n'split',\n'spoil',\n'sponsor',\n'spoon',\n'sport',\n'spot',\n'spray',\n'spread',\n'spring',\n'spy',\n'square',\n'squeeze',\n'squirrel',\n'stable',\n'stadium',\n'staff',\n'stage',\n'stairs',\n'stamp',\n'stand',\n'start',\n'state',\n'stay',\n'steak',\n'steel',\n'stem',\n'step',\n'stereo',\n'stick',\n'still',\n'sting',\n'stock',\n'stomach',\n'stone',\n'stool',\n'story',\n'stove',\n'strategy',\n'street',\n'strike',\n'strong',\n'struggle',\n'student',\n'stuff',\n'stumble',\n'style',\n'subject',\n'submit',\n'subway',\n'success',\n'such',\n'sudden',\n'suffer',\n'sugar',\n'suggest',\n'suit',\n'summer',\n'sun',\n'sunny',\n'sunset',\n'super',\n'supply',\n'supreme',\n'sure',\n'surface',\n'surge',\n'surprise',\n'surround',\n'survey',\n'suspect',\n'sustain',\n'swallow',\n'swamp',\n'swap',\n'swarm',\n'swear',\n'sweet',\n'swift',\n'swim',\n'swing',\n'switch',\n'sword',\n'symbol',\n'symptom',\n'syrup',\n'system',\n'table',\n'tackle',\n'tag',\n'tail',\n'talent',\n'talk',\n'tank',\n'tape',\n'target',\n'task',\n'taste',\n'tattoo',\n'taxi',\n'teach',\n'team',\n'tell',\n'ten',\n'tenant',\n'tennis',\n'tent',\n'term',\n'test',\n'text',\n'thank',\n'that',\n'theme',\n'then',\n'theory',\n'there',\n'they',\n'thing',\n'this',\n'thought',\n'three',\n'thrive',\n'throw',\n'thumb',\n'thunder',\n'ticket',\n'tide',\n'tiger',\n'tilt',\n'timber',\n'time',\n'tiny',\n'tip',\n'tired',\n'tissue',\n'title',\n'toast',\n'tobacco',\n'today',\n'toddler',\n'toe',\n'together',\n'toilet',\n'token',\n'tomato',\n'tomorrow',\n'tone',\n'tongue',\n'tonight',\n'tool',\n'tooth',\n'top',\n'topic',\n'topple',\n'torch',\n'tornado',\n'tortoise',\n'toss',\n'total',\n'tourist',\n'toward',\n'tower',\n'town',\n'toy',\n'track',\n'trade',\n'traffic',\n'tragic',\n'train',\n'transfer',\n'trap',\n'trash',\n'travel',\n'tray',\n'treat',\n'tree',\n'trend',\n'trial',\n'tribe',\n'trick',\n'trigger',\n'trim',\n'trip',\n'trophy',\n'trouble',\n'truck',\n'true',\n'truly',\n'trumpet',\n'trust',\n'truth',\n'try',\n'tube',\n'tuition',\n'tumble',\n'tuna',\n'tunnel',\n'turkey',\n'turn',\n'turtle',\n'twelve',\n'twenty',\n'twice',\n'twin',\n'twist',\n'two',\n'type',\n'typical',\n'ugly',\n'umbrella',\n'unable',\n'unaware',\n'uncle',\n'uncover',\n'under',\n'undo',\n'unfair',\n'unfold',\n'unhappy',\n'uniform',\n'unique',\n'unit',\n'universe',\n'unknown',\n'unlock',\n'until',\n'unusual',\n'unveil',\n'update',\n'upgrade',\n'uphold',\n'upon',\n'upper',\n'upset',\n'urban',\n'urge',\n'usage',\n'use',\n'used',\n'useful',\n'useless',\n'usual',\n'utility',\n'vacant',\n'vacuum',\n'vague',\n'valid',\n'valley',\n'valve',\n'van',\n'vanish',\n'vapor',\n'various',\n'vast',\n'vault',\n'vehicle',\n'velvet',\n'vendor',\n'venture',\n'venue',\n'verb',\n'verify',\n'version',\n'very',\n'vessel',\n'veteran',\n'viable',\n'vibrant',\n'vicious',\n'victory',\n'video',\n'view',\n'village',\n'vintage',\n'violin',\n'virtual',\n'virus',\n'visa',\n'visit',\n'visual',\n'vital',\n'vivid',\n'vocal',\n'voice',\n'void',\n'volcano',\n'volume',\n'vote',\n'voyage',\n'wage',\n'wagon',\n'wait',\n'walk',\n'wall',\n'walnut',\n'want',\n'warfare',\n'warm',\n'warrior',\n'wash',\n'wasp',\n'waste',\n'water',\n'wave',\n'way',\n'wealth',\n'weapon',\n'wear',\n'weasel',\n'weather',\n'web',\n'wedding',\n'weekend',\n'weird',\n'welcome',\n'west',\n'wet',\n'whale',\n'what',\n'wheat',\n'wheel',\n'when',\n'where',\n'whip',\n'whisper',\n'wide',\n'width',\n'wife',\n'wild',\n'will',\n'win',\n'window',\n'wine',\n'wing',\n'wink',\n'winner',\n'winter',\n'wire',\n'wisdom',\n'wise',\n'wish',\n'witness',\n'wolf',\n'woman',\n'wonder',\n'wood',\n'wool',\n'word',\n'work',\n'world',\n'worry',\n'worth',\n'wrap',\n'wreck',\n'wrestle',\n'wrist',\n'write',\n'wrong',\n'yard',\n'year',\n'yellow',\n'you',\n'young',\n'youth',\n'zebra',\n'zero',\n'zone',\n'zoo'\n]\n"
  },
  {
    "path": "lbry/wallet/words/japanese.py",
    "content": "words = [\n'あいこくしん',\n'あいさつ',\n'あいだ',\n'あおぞら',\n'あかちゃん',\n'あきる',\n'あけがた',\n'あける',\n'あこがれる',\n'あさい',\n'あさひ',\n'あしあと',\n'あじわう',\n'あずかる',\n'あずき',\n'あそぶ',\n'あたえる',\n'あたためる',\n'あたりまえ',\n'あたる',\n'あつい',\n'あつかう',\n'あっしゅく',\n'あつまり',\n'あつめる',\n'あてな',\n'あてはまる',\n'あひる',\n'あぶら',\n'あぶる',\n'あふれる',\n'あまい',\n'あまど',\n'あまやかす',\n'あまり',\n'あみもの',\n'あめりか',\n'あやまる',\n'あゆむ',\n'あらいぐま',\n'あらし',\n'あらすじ',\n'あらためる',\n'あらゆる',\n'あらわす',\n'ありがとう',\n'あわせる',\n'あわてる',\n'あんい',\n'あんがい',\n'あんこ',\n'あんぜん',\n'あんてい',\n'あんない',\n'あんまり',\n'いいだす',\n'いおん',\n'いがい',\n'いがく',\n'いきおい',\n'いきなり',\n'いきもの',\n'いきる',\n'いくじ',\n'いくぶん',\n'いけばな',\n'いけん',\n'いこう',\n'いこく',\n'いこつ',\n'いさましい',\n'いさん',\n'いしき',\n'いじゅう',\n'いじょう',\n'いじわる',\n'いずみ',\n'いずれ',\n'いせい',\n'いせえび',\n'いせかい',\n'いせき',\n'いぜん',\n'いそうろう',\n'いそがしい',\n'いだい',\n'いだく',\n'いたずら',\n'いたみ',\n'いたりあ',\n'いちおう',\n'いちじ',\n'いちど',\n'いちば',\n'いちぶ',\n'いちりゅう',\n'いつか',\n'いっしゅん',\n'いっせい',\n'いっそう',\n'いったん',\n'いっち',\n'いってい',\n'いっぽう',\n'いてざ',\n'いてん',\n'いどう',\n'いとこ',\n'いない',\n'いなか',\n'いねむり',\n'いのち',\n'いのる',\n'いはつ',\n'いばる',\n'いはん',\n'いびき',\n'いひん',\n'いふく',\n'いへん',\n'いほう',\n'いみん',\n'いもうと',\n'いもたれ',\n'いもり',\n'いやがる',\n'いやす',\n'いよかん',\n'いよく',\n'いらい',\n'いらすと',\n'いりぐち',\n'いりょう',\n'いれい',\n'いれもの',\n'いれる',\n'いろえんぴつ',\n'いわい',\n'いわう',\n'いわかん',\n'いわば',\n'いわゆる',\n'いんげんまめ',\n'いんさつ',\n'いんしょう',\n'いんよう',\n'うえき',\n'うえる',\n'うおざ',\n'うがい',\n'うかぶ',\n'うかべる',\n'うきわ',\n'うくらいな',\n'うくれれ',\n'うけたまわる',\n'うけつけ',\n'うけとる',\n'うけもつ',\n'うける',\n'うごかす',\n'うごく',\n'うこん',\n'うさぎ',\n'うしなう',\n'うしろがみ',\n'うすい',\n'うすぎ',\n'うすぐらい',\n'うすめる',\n'うせつ',\n'うちあわせ',\n'うちがわ',\n'うちき',\n'うちゅう',\n'うっかり',\n'うつくしい',\n'うったえる',\n'うつる',\n'うどん',\n'うなぎ',\n'うなじ',\n'うなずく',\n'うなる',\n'うねる',\n'うのう',\n'うぶげ',\n'うぶごえ',\n'うまれる',\n'うめる',\n'うもう',\n'うやまう',\n'うよく',\n'うらがえす',\n'うらぐち',\n'うらない',\n'うりあげ',\n'うりきれ',\n'うるさい',\n'うれしい',\n'うれゆき',\n'うれる',\n'うろこ',\n'うわき',\n'うわさ',\n'うんこう',\n'うんちん',\n'うんてん',\n'うんどう',\n'えいえん',\n'えいが',\n'えいきょう',\n'えいご',\n'えいせい',\n'えいぶん',\n'えいよう',\n'えいわ',\n'えおり',\n'えがお',\n'えがく',\n'えきたい',\n'えくせる',\n'えしゃく',\n'えすて',\n'えつらん',\n'えのぐ',\n'えほうまき',\n'えほん',\n'えまき',\n'えもじ',\n'えもの',\n'えらい',\n'えらぶ',\n'えりあ',\n'えんえん',\n'えんかい',\n'えんぎ',\n'えんげき',\n'えんしゅう',\n'えんぜつ',\n'えんそく',\n'えんちょう',\n'えんとつ',\n'おいかける',\n'おいこす',\n'おいしい',\n'おいつく',\n'おうえん',\n'おうさま',\n'おうじ',\n'おうせつ',\n'おうたい',\n'おうふく',\n'おうべい',\n'おうよう',\n'おえる',\n'おおい',\n'おおう',\n'おおどおり',\n'おおや',\n'おおよそ',\n'おかえり',\n'おかず',\n'おがむ',\n'おかわり',\n'おぎなう',\n'おきる',\n'おくさま',\n'おくじょう',\n'おくりがな',\n'おくる',\n'おくれる',\n'おこす',\n'おこなう',\n'おこる',\n'おさえる',\n'おさない',\n'おさめる',\n'おしいれ',\n'おしえる',\n'おじぎ',\n'おじさん',\n'おしゃれ',\n'おそらく',\n'おそわる',\n'おたがい',\n'おたく',\n'おだやか',\n'おちつく',\n'おっと',\n'おつり',\n'おでかけ',\n'おとしもの',\n'おとなしい',\n'おどり',\n'おどろかす',\n'おばさん',\n'おまいり',\n'おめでとう',\n'おもいで',\n'おもう',\n'おもたい',\n'おもちゃ',\n'おやつ',\n'おやゆび',\n'およぼす',\n'おらんだ',\n'おろす',\n'おんがく',\n'おんけい',\n'おんしゃ',\n'おんせん',\n'おんだん',\n'おんちゅう',\n'おんどけい',\n'かあつ',\n'かいが',\n'がいき',\n'がいけん',\n'がいこう',\n'かいさつ',\n'かいしゃ',\n'かいすいよく',\n'かいぜん',\n'かいぞうど',\n'かいつう',\n'かいてん',\n'かいとう',\n'かいふく',\n'がいへき',\n'かいほう',\n'かいよう',\n'がいらい',\n'かいわ',\n'かえる',\n'かおり',\n'かかえる',\n'かがく',\n'かがし',\n'かがみ',\n'かくご',\n'かくとく',\n'かざる',\n'がぞう',\n'かたい',\n'かたち',\n'がちょう',\n'がっきゅう',\n'がっこう',\n'がっさん',\n'がっしょう',\n'かなざわし',\n'かのう',\n'がはく',\n'かぶか',\n'かほう',\n'かほご',\n'かまう',\n'かまぼこ',\n'かめれおん',\n'かゆい',\n'かようび',\n'からい',\n'かるい',\n'かろう',\n'かわく',\n'かわら',\n'がんか',\n'かんけい',\n'かんこう',\n'かんしゃ',\n'かんそう',\n'かんたん',\n'かんち',\n'がんばる',\n'きあい',\n'きあつ',\n'きいろ',\n'ぎいん',\n'きうい',\n'きうん',\n'きえる',\n'きおう',\n'きおく',\n'きおち',\n'きおん',\n'きかい',\n'きかく',\n'きかんしゃ',\n'ききて',\n'きくばり',\n'きくらげ',\n'きけんせい',\n'きこう',\n'きこえる',\n'きこく',\n'きさい',\n'きさく',\n'きさま',\n'きさらぎ',\n'ぎじかがく',\n'ぎしき',\n'ぎじたいけん',\n'ぎじにってい',\n'ぎじゅつしゃ',\n'きすう',\n'きせい',\n'きせき',\n'きせつ',\n'きそう',\n'きぞく',\n'きぞん',\n'きたえる',\n'きちょう',\n'きつえん',\n'ぎっちり',\n'きつつき',\n'きつね',\n'きてい',\n'きどう',\n'きどく',\n'きない',\n'きなが',\n'きなこ',\n'きぬごし',\n'きねん',\n'きのう',\n'きのした',\n'きはく',\n'きびしい',\n'きひん',\n'きふく',\n'きぶん',\n'きぼう',\n'きほん',\n'きまる',\n'きみつ',\n'きむずかしい',\n'きめる',\n'きもだめし',\n'きもち',\n'きもの',\n'きゃく',\n'きやく',\n'ぎゅうにく',\n'きよう',\n'きょうりゅう',\n'きらい',\n'きらく',\n'きりん',\n'きれい',\n'きれつ',\n'きろく',\n'ぎろん',\n'きわめる',\n'ぎんいろ',\n'きんかくじ',\n'きんじょ',\n'きんようび',\n'ぐあい',\n'くいず',\n'くうかん',\n'くうき',\n'くうぐん',\n'くうこう',\n'ぐうせい',\n'くうそう',\n'ぐうたら',\n'くうふく',\n'くうぼ',\n'くかん',\n'くきょう',\n'くげん',\n'ぐこう',\n'くさい',\n'くさき',\n'くさばな',\n'くさる',\n'くしゃみ',\n'くしょう',\n'くすのき',\n'くすりゆび',\n'くせげ',\n'くせん',\n'ぐたいてき',\n'くださる',\n'くたびれる',\n'くちこみ',\n'くちさき',\n'くつした',\n'ぐっすり',\n'くつろぐ',\n'くとうてん',\n'くどく',\n'くなん',\n'くねくね',\n'くのう',\n'くふう',\n'くみあわせ',\n'くみたてる',\n'くめる',\n'くやくしょ',\n'くらす',\n'くらべる',\n'くるま',\n'くれる',\n'くろう',\n'くわしい',\n'ぐんかん',\n'ぐんしょく',\n'ぐんたい',\n'ぐんて',\n'けあな',\n'けいかく',\n'けいけん',\n'けいこ',\n'けいさつ',\n'げいじゅつ',\n'けいたい',\n'げいのうじん',\n'けいれき',\n'けいろ',\n'けおとす',\n'けおりもの',\n'げきか',\n'げきげん',\n'げきだん',\n'げきちん',\n'げきとつ',\n'げきは',\n'げきやく',\n'げこう',\n'げこくじょう',\n'げざい',\n'けさき',\n'げざん',\n'けしき',\n'けしごむ',\n'けしょう',\n'げすと',\n'けたば',\n'けちゃっぷ',\n'けちらす',\n'けつあつ',\n'けつい',\n'けつえき',\n'けっこん',\n'けつじょ',\n'けっせき',\n'けってい',\n'けつまつ',\n'げつようび',\n'げつれい',\n'けつろん',\n'げどく',\n'けとばす',\n'けとる',\n'けなげ',\n'けなす',\n'けなみ',\n'けぬき',\n'げねつ',\n'けねん',\n'けはい',\n'げひん',\n'けぶかい',\n'げぼく',\n'けまり',\n'けみかる',\n'けむし',\n'けむり',\n'けもの',\n'けらい',\n'けろけろ',\n'けわしい',\n'けんい',\n'けんえつ',\n'けんお',\n'けんか',\n'げんき',\n'けんげん',\n'けんこう',\n'けんさく',\n'けんしゅう',\n'けんすう',\n'げんそう',\n'けんちく',\n'けんてい',\n'けんとう',\n'けんない',\n'けんにん',\n'げんぶつ',\n'けんま',\n'けんみん',\n'けんめい',\n'けんらん',\n'けんり',\n'こあくま',\n'こいぬ',\n'こいびと',\n'ごうい',\n'こうえん',\n'こうおん',\n'こうかん',\n'ごうきゅう',\n'ごうけい',\n'こうこう',\n'こうさい',\n'こうじ',\n'こうすい',\n'ごうせい',\n'こうそく',\n'こうたい',\n'こうちゃ',\n'こうつう',\n'こうてい',\n'こうどう',\n'こうない',\n'こうはい',\n'ごうほう',\n'ごうまん',\n'こうもく',\n'こうりつ',\n'こえる',\n'こおり',\n'ごかい',\n'ごがつ',\n'ごかん',\n'こくご',\n'こくさい',\n'こくとう',\n'こくない',\n'こくはく',\n'こぐま',\n'こけい',\n'こける',\n'ここのか',\n'こころ',\n'こさめ',\n'こしつ',\n'こすう',\n'こせい',\n'こせき',\n'こぜん',\n'こそだて',\n'こたい',\n'こたえる',\n'こたつ',\n'こちょう',\n'こっか',\n'こつこつ',\n'こつばん',\n'こつぶ',\n'こてい',\n'こてん',\n'ことがら',\n'ことし',\n'ことば',\n'ことり',\n'こなごな',\n'こねこね',\n'このまま',\n'このみ',\n'このよ',\n'ごはん',\n'こひつじ',\n'こふう',\n'こふん',\n'こぼれる',\n'ごまあぶら',\n'こまかい',\n'ごますり',\n'こまつな',\n'こまる',\n'こむぎこ',\n'こもじ',\n'こもち',\n'こもの',\n'こもん',\n'こやく',\n'こやま',\n'こゆう',\n'こゆび',\n'こよい',\n'こよう',\n'こりる',\n'これくしょん',\n'ころっけ',\n'こわもて',\n'こわれる',\n'こんいん',\n'こんかい',\n'こんき',\n'こんしゅう',\n'こんすい',\n'こんだて',\n'こんとん',\n'こんなん',\n'こんびに',\n'こんぽん',\n'こんまけ',\n'こんや',\n'こんれい',\n'こんわく',\n'ざいえき',\n'さいかい',\n'さいきん',\n'ざいげん',\n'ざいこ',\n'さいしょ',\n'さいせい',\n'ざいたく',\n'ざいちゅう',\n'さいてき',\n'ざいりょう',\n'さうな',\n'さかいし',\n'さがす',\n'さかな',\n'さかみち',\n'さがる',\n'さぎょう',\n'さくし',\n'さくひん',\n'さくら',\n'さこく',\n'さこつ',\n'さずかる',\n'ざせき',\n'さたん',\n'さつえい',\n'ざつおん',\n'ざっか',\n'ざつがく',\n'さっきょく',\n'ざっし',\n'さつじん',\n'ざっそう',\n'さつたば',\n'さつまいも',\n'さてい',\n'さといも',\n'さとう',\n'さとおや',\n'さとし',\n'さとる',\n'さのう',\n'さばく',\n'さびしい',\n'さべつ',\n'さほう',\n'さほど',\n'さます',\n'さみしい',\n'さみだれ',\n'さむけ',\n'さめる',\n'さやえんどう',\n'さゆう',\n'さよう',\n'さよく',\n'さらだ',\n'ざるそば',\n'さわやか',\n'さわる',\n'さんいん',\n'さんか',\n'さんきゃく',\n'さんこう',\n'さんさい',\n'ざんしょ',\n'さんすう',\n'さんせい',\n'さんそ',\n'さんち',\n'さんま',\n'さんみ',\n'さんらん',\n'しあい',\n'しあげ',\n'しあさって',\n'しあわせ',\n'しいく',\n'しいん',\n'しうち',\n'しえい',\n'しおけ',\n'しかい',\n'しかく',\n'じかん',\n'しごと',\n'しすう',\n'じだい',\n'したうけ',\n'したぎ',\n'したて',\n'したみ',\n'しちょう',\n'しちりん',\n'しっかり',\n'しつじ',\n'しつもん',\n'してい',\n'してき',\n'してつ',\n'じてん',\n'じどう',\n'しなぎれ',\n'しなもの',\n'しなん',\n'しねま',\n'しねん',\n'しのぐ',\n'しのぶ',\n'しはい',\n'しばかり',\n'しはつ',\n'しはらい',\n'しはん',\n'しひょう',\n'しふく',\n'じぶん',\n'しへい',\n'しほう',\n'しほん',\n'しまう',\n'しまる',\n'しみん',\n'しむける',\n'じむしょ',\n'しめい',\n'しめる',\n'しもん',\n'しゃいん',\n'しゃうん',\n'しゃおん',\n'じゃがいも',\n'しやくしょ',\n'しゃくほう',\n'しゃけん',\n'しゃこ',\n'しゃざい',\n'しゃしん',\n'しゃせん',\n'しゃそう',\n'しゃたい',\n'しゃちょう',\n'しゃっきん',\n'じゃま',\n'しゃりん',\n'しゃれい',\n'じゆう',\n'じゅうしょ',\n'しゅくはく',\n'じゅしん',\n'しゅっせき',\n'しゅみ',\n'しゅらば',\n'じゅんばん',\n'しょうかい',\n'しょくたく',\n'しょっけん',\n'しょどう',\n'しょもつ',\n'しらせる',\n'しらべる',\n'しんか',\n'しんこう',\n'じんじゃ',\n'しんせいじ',\n'しんちく',\n'しんりん',\n'すあげ',\n'すあし',\n'すあな',\n'ずあん',\n'すいえい',\n'すいか',\n'すいとう',\n'ずいぶん',\n'すいようび',\n'すうがく',\n'すうじつ',\n'すうせん',\n'すおどり',\n'すきま',\n'すくう',\n'すくない',\n'すける',\n'すごい',\n'すこし',\n'ずさん',\n'すずしい',\n'すすむ',\n'すすめる',\n'すっかり',\n'ずっしり',\n'ずっと',\n'すてき',\n'すてる',\n'すねる',\n'すのこ',\n'すはだ',\n'すばらしい',\n'ずひょう',\n'ずぶぬれ',\n'すぶり',\n'すふれ',\n'すべて',\n'すべる',\n'ずほう',\n'すぼん',\n'すまい',\n'すめし',\n'すもう',\n'すやき',\n'すらすら',\n'するめ',\n'すれちがう',\n'すろっと',\n'すわる',\n'すんぜん',\n'すんぽう',\n'せあぶら',\n'せいかつ',\n'せいげん',\n'せいじ',\n'せいよう',\n'せおう',\n'せかいかん',\n'せきにん',\n'せきむ',\n'せきゆ',\n'せきらんうん',\n'せけん',\n'せこう',\n'せすじ',\n'せたい',\n'せたけ',\n'せっかく',\n'せっきゃく',\n'ぜっく',\n'せっけん',\n'せっこつ',\n'せっさたくま',\n'せつぞく',\n'せつだん',\n'せつでん',\n'せっぱん',\n'せつび',\n'せつぶん',\n'せつめい',\n'せつりつ',\n'せなか',\n'せのび',\n'せはば',\n'せびろ',\n'せぼね',\n'せまい',\n'せまる',\n'せめる',\n'せもたれ',\n'せりふ',\n'ぜんあく',\n'せんい',\n'せんえい',\n'せんか',\n'せんきょ',\n'せんく',\n'せんげん',\n'ぜんご',\n'せんさい',\n'せんしゅ',\n'せんすい',\n'せんせい',\n'せんぞ',\n'せんたく',\n'せんちょう',\n'せんてい',\n'せんとう',\n'せんぬき',\n'せんねん',\n'せんぱい',\n'ぜんぶ',\n'ぜんぽう',\n'せんむ',\n'せんめんじょ',\n'せんもん',\n'せんやく',\n'せんゆう',\n'せんよう',\n'ぜんら',\n'ぜんりゃく',\n'せんれい',\n'せんろ',\n'そあく',\n'そいとげる',\n'そいね',\n'そうがんきょう',\n'そうき',\n'そうご',\n'そうしん',\n'そうだん',\n'そうなん',\n'そうび',\n'そうめん',\n'そうり',\n'そえもの',\n'そえん',\n'そがい',\n'そげき',\n'そこう',\n'そこそこ',\n'そざい',\n'そしな',\n'そせい',\n'そせん',\n'そそぐ',\n'そだてる',\n'そつう',\n'そつえん',\n'そっかん',\n'そつぎょう',\n'そっけつ',\n'そっこう',\n'そっせん',\n'そっと',\n'そとがわ',\n'そとづら',\n'そなえる',\n'そなた',\n'そふぼ',\n'そぼく',\n'そぼろ',\n'そまつ',\n'そまる',\n'そむく',\n'そむりえ',\n'そめる',\n'そもそも',\n'そよかぜ',\n'そらまめ',\n'そろう',\n'そんかい',\n'そんけい',\n'そんざい',\n'そんしつ',\n'そんぞく',\n'そんちょう',\n'ぞんび',\n'ぞんぶん',\n'そんみん',\n'たあい',\n'たいいん',\n'たいうん',\n'たいえき',\n'たいおう',\n'だいがく',\n'たいき',\n'たいぐう',\n'たいけん',\n'たいこ',\n'たいざい',\n'だいじょうぶ',\n'だいすき',\n'たいせつ',\n'たいそう',\n'だいたい',\n'たいちょう',\n'たいてい',\n'だいどころ',\n'たいない',\n'たいねつ',\n'たいのう',\n'たいはん',\n'だいひょう',\n'たいふう',\n'たいへん',\n'たいほ',\n'たいまつばな',\n'たいみんぐ',\n'たいむ',\n'たいめん',\n'たいやき',\n'たいよう',\n'たいら',\n'たいりょく',\n'たいる',\n'たいわん',\n'たうえ',\n'たえる',\n'たおす',\n'たおる',\n'たおれる',\n'たかい',\n'たかね',\n'たきび',\n'たくさん',\n'たこく',\n'たこやき',\n'たさい',\n'たしざん',\n'だじゃれ',\n'たすける',\n'たずさわる',\n'たそがれ',\n'たたかう',\n'たたく',\n'ただしい',\n'たたみ',\n'たちばな',\n'だっかい',\n'だっきゃく',\n'だっこ',\n'だっしゅつ',\n'だったい',\n'たてる',\n'たとえる',\n'たなばた',\n'たにん',\n'たぬき',\n'たのしみ',\n'たはつ',\n'たぶん',\n'たべる',\n'たぼう',\n'たまご',\n'たまる',\n'だむる',\n'ためいき',\n'ためす',\n'ためる',\n'たもつ',\n'たやすい',\n'たよる',\n'たらす',\n'たりきほんがん',\n'たりょう',\n'たりる',\n'たると',\n'たれる',\n'たれんと',\n'たろっと',\n'たわむれる',\n'だんあつ',\n'たんい',\n'たんおん',\n'たんか',\n'たんき',\n'たんけん',\n'たんご',\n'たんさん',\n'たんじょうび',\n'だんせい',\n'たんそく',\n'たんたい',\n'だんち',\n'たんてい',\n'たんとう',\n'だんな',\n'たんにん',\n'だんねつ',\n'たんのう',\n'たんぴん',\n'だんぼう',\n'たんまつ',\n'たんめい',\n'だんれつ',\n'だんろ',\n'だんわ',\n'ちあい',\n'ちあん',\n'ちいき',\n'ちいさい',\n'ちえん',\n'ちかい',\n'ちから',\n'ちきゅう',\n'ちきん',\n'ちけいず',\n'ちけん',\n'ちこく',\n'ちさい',\n'ちしき',\n'ちしりょう',\n'ちせい',\n'ちそう',\n'ちたい',\n'ちたん',\n'ちちおや',\n'ちつじょ',\n'ちてき',\n'ちてん',\n'ちぬき',\n'ちぬり',\n'ちのう',\n'ちひょう',\n'ちへいせん',\n'ちほう',\n'ちまた',\n'ちみつ',\n'ちみどろ',\n'ちめいど',\n'ちゃんこなべ',\n'ちゅうい',\n'ちゆりょく',\n'ちょうし',\n'ちょさくけん',\n'ちらし',\n'ちらみ',\n'ちりがみ',\n'ちりょう',\n'ちるど',\n'ちわわ',\n'ちんたい',\n'ちんもく',\n'ついか',\n'ついたち',\n'つうか',\n'つうじょう',\n'つうはん',\n'つうわ',\n'つかう',\n'つかれる',\n'つくね',\n'つくる',\n'つけね',\n'つける',\n'つごう',\n'つたえる',\n'つづく',\n'つつじ',\n'つつむ',\n'つとめる',\n'つながる',\n'つなみ',\n'つねづね',\n'つのる',\n'つぶす',\n'つまらない',\n'つまる',\n'つみき',\n'つめたい',\n'つもり',\n'つもる',\n'つよい',\n'つるぼ',\n'つるみく',\n'つわもの',\n'つわり',\n'てあし',\n'てあて',\n'てあみ',\n'ていおん',\n'ていか',\n'ていき',\n'ていけい',\n'ていこく',\n'ていさつ',\n'ていし',\n'ていせい',\n'ていたい',\n'ていど',\n'ていねい',\n'ていひょう',\n'ていへん',\n'ていぼう',\n'てうち',\n'ておくれ',\n'てきとう',\n'てくび',\n'でこぼこ',\n'てさぎょう',\n'てさげ',\n'てすり',\n'てそう',\n'てちがい',\n'てちょう',\n'てつがく',\n'てつづき',\n'でっぱ',\n'てつぼう',\n'てつや',\n'でぬかえ',\n'てぬき',\n'てぬぐい',\n'てのひら',\n'てはい',\n'てぶくろ',\n'てふだ',\n'てほどき',\n'てほん',\n'てまえ',\n'てまきずし',\n'てみじか',\n'てみやげ',\n'てらす',\n'てれび',\n'てわけ',\n'てわたし',\n'でんあつ',\n'てんいん',\n'てんかい',\n'てんき',\n'てんぐ',\n'てんけん',\n'てんごく',\n'てんさい',\n'てんし',\n'てんすう',\n'でんち',\n'てんてき',\n'てんとう',\n'てんない',\n'てんぷら',\n'てんぼうだい',\n'てんめつ',\n'てんらんかい',\n'でんりょく',\n'でんわ',\n'どあい',\n'といれ',\n'どうかん',\n'とうきゅう',\n'どうぐ',\n'とうし',\n'とうむぎ',\n'とおい',\n'とおか',\n'とおく',\n'とおす',\n'とおる',\n'とかい',\n'とかす',\n'ときおり',\n'ときどき',\n'とくい',\n'とくしゅう',\n'とくてん',\n'とくに',\n'とくべつ',\n'とけい',\n'とける',\n'とこや',\n'とさか',\n'としょかん',\n'とそう',\n'とたん',\n'とちゅう',\n'とっきゅう',\n'とっくん',\n'とつぜん',\n'とつにゅう',\n'とどける',\n'ととのえる',\n'とない',\n'となえる',\n'となり',\n'とのさま',\n'とばす',\n'どぶがわ',\n'とほう',\n'とまる',\n'とめる',\n'ともだち',\n'ともる',\n'どようび',\n'とらえる',\n'とんかつ',\n'どんぶり',\n'ないかく',\n'ないこう',\n'ないしょ',\n'ないす',\n'ないせん',\n'ないそう',\n'なおす',\n'ながい',\n'なくす',\n'なげる',\n'なこうど',\n'なさけ',\n'なたでここ',\n'なっとう',\n'なつやすみ',\n'ななおし',\n'なにごと',\n'なにもの',\n'なにわ',\n'なのか',\n'なふだ',\n'なまいき',\n'なまえ',\n'なまみ',\n'なみだ',\n'なめらか',\n'なめる',\n'なやむ',\n'ならう',\n'ならび',\n'ならぶ',\n'なれる',\n'なわとび',\n'なわばり',\n'にあう',\n'にいがた',\n'にうけ',\n'におい',\n'にかい',\n'にがて',\n'にきび',\n'にくしみ',\n'にくまん',\n'にげる',\n'にさんかたんそ',\n'にしき',\n'にせもの',\n'にちじょう',\n'にちようび',\n'にっか',\n'にっき',\n'にっけい',\n'にっこう',\n'にっさん',\n'にっしょく',\n'にっすう',\n'にっせき',\n'にってい',\n'になう',\n'にほん',\n'にまめ',\n'にもつ',\n'にやり',\n'にゅういん',\n'にりんしゃ',\n'にわとり',\n'にんい',\n'にんか',\n'にんき',\n'にんげん',\n'にんしき',\n'にんずう',\n'にんそう',\n'にんたい',\n'にんち',\n'にんてい',\n'にんにく',\n'にんぷ',\n'にんまり',\n'にんむ',\n'にんめい',\n'にんよう',\n'ぬいくぎ',\n'ぬかす',\n'ぬぐいとる',\n'ぬぐう',\n'ぬくもり',\n'ぬすむ',\n'ぬまえび',\n'ぬめり',\n'ぬらす',\n'ぬんちゃく',\n'ねあげ',\n'ねいき',\n'ねいる',\n'ねいろ',\n'ねぐせ',\n'ねくたい',\n'ねくら',\n'ねこぜ',\n'ねこむ',\n'ねさげ',\n'ねすごす',\n'ねそべる',\n'ねだん',\n'ねつい',\n'ねっしん',\n'ねつぞう',\n'ねったいぎょ',\n'ねぶそく',\n'ねふだ',\n'ねぼう',\n'ねほりはほり',\n'ねまき',\n'ねまわし',\n'ねみみ',\n'ねむい',\n'ねむたい',\n'ねもと',\n'ねらう',\n'ねわざ',\n'ねんいり',\n'ねんおし',\n'ねんかん',\n'ねんきん',\n'ねんぐ',\n'ねんざ',\n'ねんし',\n'ねんちゃく',\n'ねんど',\n'ねんぴ',\n'ねんぶつ',\n'ねんまつ',\n'ねんりょう',\n'ねんれい',\n'のいず',\n'のおづま',\n'のがす',\n'のきなみ',\n'のこぎり',\n'のこす',\n'のこる',\n'のせる',\n'のぞく',\n'のぞむ',\n'のたまう',\n'のちほど',\n'のっく',\n'のばす',\n'のはら',\n'のべる',\n'のぼる',\n'のみもの',\n'のやま',\n'のらいぬ',\n'のらねこ',\n'のりもの',\n'のりゆき',\n'のれん',\n'のんき',\n'ばあい',\n'はあく',\n'ばあさん',\n'ばいか',\n'ばいく',\n'はいけん',\n'はいご',\n'はいしん',\n'はいすい',\n'はいせん',\n'はいそう',\n'はいち',\n'ばいばい',\n'はいれつ',\n'はえる',\n'はおる',\n'はかい',\n'ばかり',\n'はかる',\n'はくしゅ',\n'はけん',\n'はこぶ',\n'はさみ',\n'はさん',\n'はしご',\n'ばしょ',\n'はしる',\n'はせる',\n'ぱそこん',\n'はそん',\n'はたん',\n'はちみつ',\n'はつおん',\n'はっかく',\n'はづき',\n'はっきり',\n'はっくつ',\n'はっけん',\n'はっこう',\n'はっさん',\n'はっしん',\n'はったつ',\n'はっちゅう',\n'はってん',\n'はっぴょう',\n'はっぽう',\n'はなす',\n'はなび',\n'はにかむ',\n'はぶらし',\n'はみがき',\n'はむかう',\n'はめつ',\n'はやい',\n'はやし',\n'はらう',\n'はろうぃん',\n'はわい',\n'はんい',\n'はんえい',\n'はんおん',\n'はんかく',\n'はんきょう',\n'ばんぐみ',\n'はんこ',\n'はんしゃ',\n'はんすう',\n'はんだん',\n'ぱんち',\n'ぱんつ',\n'はんてい',\n'はんとし',\n'はんのう',\n'はんぱ',\n'はんぶん',\n'はんぺん',\n'はんぼうき',\n'はんめい',\n'はんらん',\n'はんろん',\n'ひいき',\n'ひうん',\n'ひえる',\n'ひかく',\n'ひかり',\n'ひかる',\n'ひかん',\n'ひくい',\n'ひけつ',\n'ひこうき',\n'ひこく',\n'ひさい',\n'ひさしぶり',\n'ひさん',\n'びじゅつかん',\n'ひしょ',\n'ひそか',\n'ひそむ',\n'ひたむき',\n'ひだり',\n'ひたる',\n'ひつぎ',\n'ひっこし',\n'ひっし',\n'ひつじゅひん',\n'ひっす',\n'ひつぜん',\n'ぴったり',\n'ぴっちり',\n'ひつよう',\n'ひてい',\n'ひとごみ',\n'ひなまつり',\n'ひなん',\n'ひねる',\n'ひはん',\n'ひびく',\n'ひひょう',\n'ひほう',\n'ひまわり',\n'ひまん',\n'ひみつ',\n'ひめい',\n'ひめじし',\n'ひやけ',\n'ひやす',\n'ひよう',\n'びょうき',\n'ひらがな',\n'ひらく',\n'ひりつ',\n'ひりょう',\n'ひるま',\n'ひるやすみ',\n'ひれい',\n'ひろい',\n'ひろう',\n'ひろき',\n'ひろゆき',\n'ひんかく',\n'ひんけつ',\n'ひんこん',\n'ひんしゅ',\n'ひんそう',\n'ぴんち',\n'ひんぱん',\n'びんぼう',\n'ふあん',\n'ふいうち',\n'ふうけい',\n'ふうせん',\n'ぷうたろう',\n'ふうとう',\n'ふうふ',\n'ふえる',\n'ふおん',\n'ふかい',\n'ふきん',\n'ふくざつ',\n'ふくぶくろ',\n'ふこう',\n'ふさい',\n'ふしぎ',\n'ふじみ',\n'ふすま',\n'ふせい',\n'ふせぐ',\n'ふそく',\n'ぶたにく',\n'ふたん',\n'ふちょう',\n'ふつう',\n'ふつか',\n'ふっかつ',\n'ふっき',\n'ふっこく',\n'ぶどう',\n'ふとる',\n'ふとん',\n'ふのう',\n'ふはい',\n'ふひょう',\n'ふへん',\n'ふまん',\n'ふみん',\n'ふめつ',\n'ふめん',\n'ふよう',\n'ふりこ',\n'ふりる',\n'ふるい',\n'ふんいき',\n'ぶんがく',\n'ぶんぐ',\n'ふんしつ',\n'ぶんせき',\n'ふんそう',\n'ぶんぽう',\n'へいあん',\n'へいおん',\n'へいがい',\n'へいき',\n'へいげん',\n'へいこう',\n'へいさ',\n'へいしゃ',\n'へいせつ',\n'へいそ',\n'へいたく',\n'へいてん',\n'へいねつ',\n'へいわ',\n'へきが',\n'へこむ',\n'べにいろ',\n'べにしょうが',\n'へらす',\n'へんかん',\n'べんきょう',\n'べんごし',\n'へんさい',\n'へんたい',\n'べんり',\n'ほあん',\n'ほいく',\n'ぼうぎょ',\n'ほうこく',\n'ほうそう',\n'ほうほう',\n'ほうもん',\n'ほうりつ',\n'ほえる',\n'ほおん',\n'ほかん',\n'ほきょう',\n'ぼきん',\n'ほくろ',\n'ほけつ',\n'ほけん',\n'ほこう',\n'ほこる',\n'ほしい',\n'ほしつ',\n'ほしゅ',\n'ほしょう',\n'ほせい',\n'ほそい',\n'ほそく',\n'ほたて',\n'ほたる',\n'ぽちぶくろ',\n'ほっきょく',\n'ほっさ',\n'ほったん',\n'ほとんど',\n'ほめる',\n'ほんい',\n'ほんき',\n'ほんけ',\n'ほんしつ',\n'ほんやく',\n'まいにち',\n'まかい',\n'まかせる',\n'まがる',\n'まける',\n'まこと',\n'まさつ',\n'まじめ',\n'ますく',\n'まぜる',\n'まつり',\n'まとめ',\n'まなぶ',\n'まぬけ',\n'まねく',\n'まほう',\n'まもる',\n'まゆげ',\n'まよう',\n'まろやか',\n'まわす',\n'まわり',\n'まわる',\n'まんが',\n'まんきつ',\n'まんぞく',\n'まんなか',\n'みいら',\n'みうち',\n'みえる',\n'みがく',\n'みかた',\n'みかん',\n'みけん',\n'みこん',\n'みじかい',\n'みすい',\n'みすえる',\n'みせる',\n'みっか',\n'みつかる',\n'みつける',\n'みてい',\n'みとめる',\n'みなと',\n'みなみかさい',\n'みねらる',\n'みのう',\n'みのがす',\n'みほん',\n'みもと',\n'みやげ',\n'みらい',\n'みりょく',\n'みわく',\n'みんか',\n'みんぞく',\n'むいか',\n'むえき',\n'むえん',\n'むかい',\n'むかう',\n'むかえ',\n'むかし',\n'むぎちゃ',\n'むける',\n'むげん',\n'むさぼる',\n'むしあつい',\n'むしば',\n'むじゅん',\n'むしろ',\n'むすう',\n'むすこ',\n'むすぶ',\n'むすめ',\n'むせる',\n'むせん',\n'むちゅう',\n'むなしい',\n'むのう',\n'むやみ',\n'むよう',\n'むらさき',\n'むりょう',\n'むろん',\n'めいあん',\n'めいうん',\n'めいえん',\n'めいかく',\n'めいきょく',\n'めいさい',\n'めいし',\n'めいそう',\n'めいぶつ',\n'めいれい',\n'めいわく',\n'めぐまれる',\n'めざす',\n'めした',\n'めずらしい',\n'めだつ',\n'めまい',\n'めやす',\n'めんきょ',\n'めんせき',\n'めんどう',\n'もうしあげる',\n'もうどうけん',\n'もえる',\n'もくし',\n'もくてき',\n'もくようび',\n'もちろん',\n'もどる',\n'もらう',\n'もんく',\n'もんだい',\n'やおや',\n'やける',\n'やさい',\n'やさしい',\n'やすい',\n'やすたろう',\n'やすみ',\n'やせる',\n'やそう',\n'やたい',\n'やちん',\n'やっと',\n'やっぱり',\n'やぶる',\n'やめる',\n'ややこしい',\n'やよい',\n'やわらかい',\n'ゆうき',\n'ゆうびんきょく',\n'ゆうべ',\n'ゆうめい',\n'ゆけつ',\n'ゆしゅつ',\n'ゆせん',\n'ゆそう',\n'ゆたか',\n'ゆちゃく',\n'ゆでる',\n'ゆにゅう',\n'ゆびわ',\n'ゆらい',\n'ゆれる',\n'ようい',\n'ようか',\n'ようきゅう',\n'ようじ',\n'ようす',\n'ようちえん',\n'よかぜ',\n'よかん',\n'よきん',\n'よくせい',\n'よくぼう',\n'よけい',\n'よごれる',\n'よさん',\n'よしゅう',\n'よそう',\n'よそく',\n'よっか',\n'よてい',\n'よどがわく',\n'よねつ',\n'よやく',\n'よゆう',\n'よろこぶ',\n'よろしい',\n'らいう',\n'らくがき',\n'らくご',\n'らくさつ',\n'らくだ',\n'らしんばん',\n'らせん',\n'らぞく',\n'らたい',\n'らっか',\n'られつ',\n'りえき',\n'りかい',\n'りきさく',\n'りきせつ',\n'りくぐん',\n'りくつ',\n'りけん',\n'りこう',\n'りせい',\n'りそう',\n'りそく',\n'りてん',\n'りねん',\n'りゆう',\n'りゅうがく',\n'りよう',\n'りょうり',\n'りょかん',\n'りょくちゃ',\n'りょこう',\n'りりく',\n'りれき',\n'りろん',\n'りんご',\n'るいけい',\n'るいさい',\n'るいじ',\n'るいせき',\n'るすばん',\n'るりがわら',\n'れいかん',\n'れいぎ',\n'れいせい',\n'れいぞうこ',\n'れいとう',\n'れいぼう',\n'れきし',\n'れきだい',\n'れんあい',\n'れんけい',\n'れんこん',\n'れんさい',\n'れんしゅう',\n'れんぞく',\n'れんらく',\n'ろうか',\n'ろうご',\n'ろうじん',\n'ろうそく',\n'ろくが',\n'ろこつ',\n'ろじうら',\n'ろしゅつ',\n'ろせん',\n'ろてん',\n'ろめん',\n'ろれつ',\n'ろんぎ',\n'ろんぱ',\n'ろんぶん',\n'ろんり',\n'わかす',\n'わかめ',\n'わかやま',\n'わかれる',\n'わしつ',\n'わじまし',\n'わすれもの',\n'わらう',\n'われる'\n]\n"
  },
  {
    "path": "lbry/wallet/words/portuguese.py",
    "content": "words = [\r\n'abaular',\r\n'abdominal',\r\n'abeto',\r\n'abissinio',\r\n'abjeto',\r\n'ablucao',\r\n'abnegar',\r\n'abotoar',\r\n'abrutalhar',\r\n'absurdo',\r\n'abutre',\r\n'acautelar',\r\n'accessorios',\r\n'acetona',\r\n'achocolatado',\r\n'acirrar',\r\n'acne',\r\n'acovardar',\r\n'acrostico',\r\n'actinomicete',\r\n'acustico',\r\n'adaptavel',\r\n'adeus',\r\n'adivinho',\r\n'adjunto',\r\n'admoestar',\r\n'adnominal',\r\n'adotivo',\r\n'adquirir',\r\n'adriatico',\r\n'adsorcao',\r\n'adutora',\r\n'advogar',\r\n'aerossol',\r\n'afazeres',\r\n'afetuoso',\r\n'afixo',\r\n'afluir',\r\n'afortunar',\r\n'afrouxar',\r\n'aftosa',\r\n'afunilar',\r\n'agentes',\r\n'agito',\r\n'aglutinar',\r\n'aiatola',\r\n'aimore',\r\n'aino',\r\n'aipo',\r\n'airoso',\r\n'ajeitar',\r\n'ajoelhar',\r\n'ajudante',\r\n'ajuste',\r\n'alazao',\r\n'albumina',\r\n'alcunha',\r\n'alegria',\r\n'alexandre',\r\n'alforriar',\r\n'alguns',\r\n'alhures',\r\n'alivio',\r\n'almoxarife',\r\n'alotropico',\r\n'alpiste',\r\n'alquimista',\r\n'alsaciano',\r\n'altura',\r\n'aluviao',\r\n'alvura',\r\n'amazonico',\r\n'ambulatorio',\r\n'ametodico',\r\n'amizades',\r\n'amniotico',\r\n'amovivel',\r\n'amurada',\r\n'anatomico',\r\n'ancorar',\r\n'anexo',\r\n'anfora',\r\n'aniversario',\r\n'anjo',\r\n'anotar',\r\n'ansioso',\r\n'anturio',\r\n'anuviar',\r\n'anverso',\r\n'anzol',\r\n'aonde',\r\n'apaziguar',\r\n'apito',\r\n'aplicavel',\r\n'apoteotico',\r\n'aprimorar',\r\n'aprumo',\r\n'apto',\r\n'apuros',\r\n'aquoso',\r\n'arauto',\r\n'arbusto',\r\n'arduo',\r\n'aresta',\r\n'arfar',\r\n'arguto',\r\n'aritmetico',\r\n'arlequim',\r\n'armisticio',\r\n'aromatizar',\r\n'arpoar',\r\n'arquivo',\r\n'arrumar',\r\n'arsenio',\r\n'arturiano',\r\n'aruaque',\r\n'arvores',\r\n'asbesto',\r\n'ascorbico',\r\n'aspirina',\r\n'asqueroso',\r\n'assustar',\r\n'astuto',\r\n'atazanar',\r\n'ativo',\r\n'atletismo',\r\n'atmosferico',\r\n'atormentar',\r\n'atroz',\r\n'aturdir',\r\n'audivel',\r\n'auferir',\r\n'augusto',\r\n'aula',\r\n'aumento',\r\n'aurora',\r\n'autuar',\r\n'avatar',\r\n'avexar',\r\n'avizinhar',\r\n'avolumar',\r\n'avulso',\r\n'axiomatico',\r\n'azerbaijano',\r\n'azimute',\r\n'azoto',\r\n'azulejo',\r\n'bacteriologista',\r\n'badulaque',\r\n'baforada',\r\n'baixote',\r\n'bajular',\r\n'balzaquiana',\r\n'bambuzal',\r\n'banzo',\r\n'baoba',\r\n'baqueta',\r\n'barulho',\r\n'bastonete',\r\n'batuta',\r\n'bauxita',\r\n'bavaro',\r\n'bazuca',\r\n'bcrepuscular',\r\n'beato',\r\n'beduino',\r\n'begonia',\r\n'behaviorista',\r\n'beisebol',\r\n'belzebu',\r\n'bemol',\r\n'benzido',\r\n'beocio',\r\n'bequer',\r\n'berro',\r\n'besuntar',\r\n'betume',\r\n'bexiga',\r\n'bezerro',\r\n'biatlon',\r\n'biboca',\r\n'bicuspide',\r\n'bidirecional',\r\n'bienio',\r\n'bifurcar',\r\n'bigorna',\r\n'bijuteria',\r\n'bimotor',\r\n'binormal',\r\n'bioxido',\r\n'bipolarizacao',\r\n'biquini',\r\n'birutice',\r\n'bisturi',\r\n'bituca',\r\n'biunivoco',\r\n'bivalve',\r\n'bizarro',\r\n'blasfemo',\r\n'blenorreia',\r\n'blindar',\r\n'bloqueio',\r\n'blusao',\r\n'boazuda',\r\n'bofete',\r\n'bojudo',\r\n'bolso',\r\n'bombordo',\r\n'bonzo',\r\n'botina',\r\n'boquiaberto',\r\n'bostoniano',\r\n'botulismo',\r\n'bourbon',\r\n'bovino',\r\n'boximane',\r\n'bravura',\r\n'brevidade',\r\n'britar',\r\n'broxar',\r\n'bruno',\r\n'bruxuleio',\r\n'bubonico',\r\n'bucolico',\r\n'buda',\r\n'budista',\r\n'bueiro',\r\n'buffer',\r\n'bugre',\r\n'bujao',\r\n'bumerangue',\r\n'burundines',\r\n'busto',\r\n'butique',\r\n'buzios',\r\n'caatinga',\r\n'cabuqui',\r\n'cacunda',\r\n'cafuzo',\r\n'cajueiro',\r\n'camurca',\r\n'canudo',\r\n'caquizeiro',\r\n'carvoeiro',\r\n'casulo',\r\n'catuaba',\r\n'cauterizar',\r\n'cebolinha',\r\n'cedula',\r\n'ceifeiro',\r\n'celulose',\r\n'cerzir',\r\n'cesto',\r\n'cetro',\r\n'ceus',\r\n'cevar',\r\n'chavena',\r\n'cheroqui',\r\n'chita',\r\n'chovido',\r\n'chuvoso',\r\n'ciatico',\r\n'cibernetico',\r\n'cicuta',\r\n'cidreira',\r\n'cientistas',\r\n'cifrar',\r\n'cigarro',\r\n'cilio',\r\n'cimo',\r\n'cinzento',\r\n'cioso',\r\n'cipriota',\r\n'cirurgico',\r\n'cisto',\r\n'citrico',\r\n'ciumento',\r\n'civismo',\r\n'clavicula',\r\n'clero',\r\n'clitoris',\r\n'cluster',\r\n'coaxial',\r\n'cobrir',\r\n'cocota',\r\n'codorniz',\r\n'coexistir',\r\n'cogumelo',\r\n'coito',\r\n'colusao',\r\n'compaixao',\r\n'comutativo',\r\n'contentamento',\r\n'convulsivo',\r\n'coordenativa',\r\n'coquetel',\r\n'correto',\r\n'corvo',\r\n'costureiro',\r\n'cotovia',\r\n'covil',\r\n'cozinheiro',\r\n'cretino',\r\n'cristo',\r\n'crivo',\r\n'crotalo',\r\n'cruzes',\r\n'cubo',\r\n'cucuia',\r\n'cueiro',\r\n'cuidar',\r\n'cujo',\r\n'cultural',\r\n'cunilingua',\r\n'cupula',\r\n'curvo',\r\n'custoso',\r\n'cutucar',\r\n'czarismo',\r\n'dablio',\r\n'dacota',\r\n'dados',\r\n'daguerreotipo',\r\n'daiquiri',\r\n'daltonismo',\r\n'damista',\r\n'dantesco',\r\n'daquilo',\r\n'darwinista',\r\n'dasein',\r\n'dativo',\r\n'deao',\r\n'debutantes',\r\n'decurso',\r\n'deduzir',\r\n'defunto',\r\n'degustar',\r\n'dejeto',\r\n'deltoide',\r\n'demover',\r\n'denunciar',\r\n'deputado',\r\n'deque',\r\n'dervixe',\r\n'desvirtuar',\r\n'deturpar',\r\n'deuteronomio',\r\n'devoto',\r\n'dextrose',\r\n'dezoito',\r\n'diatribe',\r\n'dicotomico',\r\n'didatico',\r\n'dietista',\r\n'difuso',\r\n'digressao',\r\n'diluvio',\r\n'diminuto',\r\n'dinheiro',\r\n'dinossauro',\r\n'dioxido',\r\n'diplomatico',\r\n'dique',\r\n'dirimivel',\r\n'disturbio',\r\n'diurno',\r\n'divulgar',\r\n'dizivel',\r\n'doar',\r\n'dobro',\r\n'docura',\r\n'dodoi',\r\n'doer',\r\n'dogue',\r\n'doloso',\r\n'domo',\r\n'donzela',\r\n'doping',\r\n'dorsal',\r\n'dossie',\r\n'dote',\r\n'doutro',\r\n'doze',\r\n'dravidico',\r\n'dreno',\r\n'driver',\r\n'dropes',\r\n'druso',\r\n'dubnio',\r\n'ducto',\r\n'dueto',\r\n'dulija',\r\n'dundum',\r\n'duodeno',\r\n'duquesa',\r\n'durou',\r\n'duvidoso',\r\n'duzia',\r\n'ebano',\r\n'ebrio',\r\n'eburneo',\r\n'echarpe',\r\n'eclusa',\r\n'ecossistema',\r\n'ectoplasma',\r\n'ecumenismo',\r\n'eczema',\r\n'eden',\r\n'editorial',\r\n'edredom',\r\n'edulcorar',\r\n'efetuar',\r\n'efigie',\r\n'efluvio',\r\n'egiptologo',\r\n'egresso',\r\n'egua',\r\n'einsteiniano',\r\n'eira',\r\n'eivar',\r\n'eixos',\r\n'ejetar',\r\n'elastomero',\r\n'eldorado',\r\n'elixir',\r\n'elmo',\r\n'eloquente',\r\n'elucidativo',\r\n'emaranhar',\r\n'embutir',\r\n'emerito',\r\n'emfa',\r\n'emitir',\r\n'emotivo',\r\n'empuxo',\r\n'emulsao',\r\n'enamorar',\r\n'encurvar',\r\n'enduro',\r\n'enevoar',\r\n'enfurnar',\r\n'enguico',\r\n'enho',\r\n'enigmista',\r\n'enlutar',\r\n'enormidade',\r\n'enpreendimento',\r\n'enquanto',\r\n'enriquecer',\r\n'enrugar',\r\n'entusiastico',\r\n'enunciar',\r\n'envolvimento',\r\n'enxuto',\r\n'enzimatico',\r\n'eolico',\r\n'epiteto',\r\n'epoxi',\r\n'epura',\r\n'equivoco',\r\n'erario',\r\n'erbio',\r\n'ereto',\r\n'erguido',\r\n'erisipela',\r\n'ermo',\r\n'erotizar',\r\n'erros',\r\n'erupcao',\r\n'ervilha',\r\n'esburacar',\r\n'escutar',\r\n'esfuziante',\r\n'esguio',\r\n'esloveno',\r\n'esmurrar',\r\n'esoterismo',\r\n'esperanca',\r\n'espirito',\r\n'espurio',\r\n'essencialmente',\r\n'esturricar',\r\n'esvoacar',\r\n'etario',\r\n'eterno',\r\n'etiquetar',\r\n'etnologo',\r\n'etos',\r\n'etrusco',\r\n'euclidiano',\r\n'euforico',\r\n'eugenico',\r\n'eunuco',\r\n'europio',\r\n'eustaquio',\r\n'eutanasia',\r\n'evasivo',\r\n'eventualidade',\r\n'evitavel',\r\n'evoluir',\r\n'exaustor',\r\n'excursionista',\r\n'exercito',\r\n'exfoliado',\r\n'exito',\r\n'exotico',\r\n'expurgo',\r\n'exsudar',\r\n'extrusora',\r\n'exumar',\r\n'fabuloso',\r\n'facultativo',\r\n'fado',\r\n'fagulha',\r\n'faixas',\r\n'fajuto',\r\n'faltoso',\r\n'famoso',\r\n'fanzine',\r\n'fapesp',\r\n'faquir',\r\n'fartura',\r\n'fastio',\r\n'faturista',\r\n'fausto',\r\n'favorito',\r\n'faxineira',\r\n'fazer',\r\n'fealdade',\r\n'febril',\r\n'fecundo',\r\n'fedorento',\r\n'feerico',\r\n'feixe',\r\n'felicidade',\r\n'felipe',\r\n'feltro',\r\n'femur',\r\n'fenotipo',\r\n'fervura',\r\n'festivo',\r\n'feto',\r\n'feudo',\r\n'fevereiro',\r\n'fezinha',\r\n'fiasco',\r\n'fibra',\r\n'ficticio',\r\n'fiduciario',\r\n'fiesp',\r\n'fifa',\r\n'figurino',\r\n'fijiano',\r\n'filtro',\r\n'finura',\r\n'fiorde',\r\n'fiquei',\r\n'firula',\r\n'fissurar',\r\n'fitoteca',\r\n'fivela',\r\n'fixo',\r\n'flavio',\r\n'flexor',\r\n'flibusteiro',\r\n'flotilha',\r\n'fluxograma',\r\n'fobos',\r\n'foco',\r\n'fofura',\r\n'foguista',\r\n'foie',\r\n'foliculo',\r\n'fominha',\r\n'fonte',\r\n'forum',\r\n'fosso',\r\n'fotossintese',\r\n'foxtrote',\r\n'fraudulento',\r\n'frevo',\r\n'frivolo',\r\n'frouxo',\r\n'frutose',\r\n'fuba',\r\n'fucsia',\r\n'fugitivo',\r\n'fuinha',\r\n'fujao',\r\n'fulustreco',\r\n'fumo',\r\n'funileiro',\r\n'furunculo',\r\n'fustigar',\r\n'futurologo',\r\n'fuxico',\r\n'fuzue',\r\n'gabriel',\r\n'gado',\r\n'gaelico',\r\n'gafieira',\r\n'gaguejo',\r\n'gaivota',\r\n'gajo',\r\n'galvanoplastico',\r\n'gamo',\r\n'ganso',\r\n'garrucha',\r\n'gastronomo',\r\n'gatuno',\r\n'gaussiano',\r\n'gaviao',\r\n'gaxeta',\r\n'gazeteiro',\r\n'gear',\r\n'geiser',\r\n'geminiano',\r\n'generoso',\r\n'genuino',\r\n'geossinclinal',\r\n'gerundio',\r\n'gestual',\r\n'getulista',\r\n'gibi',\r\n'gigolo',\r\n'gilete',\r\n'ginseng',\r\n'giroscopio',\r\n'glaucio',\r\n'glacial',\r\n'gleba',\r\n'glifo',\r\n'glote',\r\n'glutonia',\r\n'gnostico',\r\n'goela',\r\n'gogo',\r\n'goitaca',\r\n'golpista',\r\n'gomo',\r\n'gonzo',\r\n'gorro',\r\n'gostou',\r\n'goticula',\r\n'gourmet',\r\n'governo',\r\n'gozo',\r\n'graxo',\r\n'grevista',\r\n'grito',\r\n'grotesco',\r\n'gruta',\r\n'guaxinim',\r\n'gude',\r\n'gueto',\r\n'guizo',\r\n'guloso',\r\n'gume',\r\n'guru',\r\n'gustativo',\r\n'gustavo',\r\n'gutural',\r\n'habitue',\r\n'haitiano',\r\n'halterofilista',\r\n'hamburguer',\r\n'hanseniase',\r\n'happening',\r\n'harpista',\r\n'hastear',\r\n'haveres',\r\n'hebreu',\r\n'hectometro',\r\n'hedonista',\r\n'hegira',\r\n'helena',\r\n'helminto',\r\n'hemorroidas',\r\n'henrique',\r\n'heptassilabo',\r\n'hertziano',\r\n'hesitar',\r\n'heterossexual',\r\n'heuristico',\r\n'hexagono',\r\n'hiato',\r\n'hibrido',\r\n'hidrostatico',\r\n'hieroglifo',\r\n'hifenizar',\r\n'higienizar',\r\n'hilario',\r\n'himen',\r\n'hino',\r\n'hippie',\r\n'hirsuto',\r\n'historiografia',\r\n'hitlerista',\r\n'hodometro',\r\n'hoje',\r\n'holograma',\r\n'homus',\r\n'honroso',\r\n'hoquei',\r\n'horto',\r\n'hostilizar',\r\n'hotentote',\r\n'huguenote',\r\n'humilde',\r\n'huno',\r\n'hurra',\r\n'hutu',\r\n'iaia',\r\n'ialorixa',\r\n'iambico',\r\n'iansa',\r\n'iaque',\r\n'iara',\r\n'iatista',\r\n'iberico',\r\n'ibis',\r\n'icar',\r\n'iceberg',\r\n'icosagono',\r\n'idade',\r\n'ideologo',\r\n'idiotice',\r\n'idoso',\r\n'iemenita',\r\n'iene',\r\n'igarape',\r\n'iglu',\r\n'ignorar',\r\n'igreja',\r\n'iguaria',\r\n'iidiche',\r\n'ilativo',\r\n'iletrado',\r\n'ilharga',\r\n'ilimitado',\r\n'ilogismo',\r\n'ilustrissimo',\r\n'imaturo',\r\n'imbuzeiro',\r\n'imerso',\r\n'imitavel',\r\n'imovel',\r\n'imputar',\r\n'imutavel',\r\n'inaveriguavel',\r\n'incutir',\r\n'induzir',\r\n'inextricavel',\r\n'infusao',\r\n'ingua',\r\n'inhame',\r\n'iniquo',\r\n'injusto',\r\n'inning',\r\n'inoxidavel',\r\n'inquisitorial',\r\n'insustentavel',\r\n'intumescimento',\r\n'inutilizavel',\r\n'invulneravel',\r\n'inzoneiro',\r\n'iodo',\r\n'iogurte',\r\n'ioio',\r\n'ionosfera',\r\n'ioruba',\r\n'iota',\r\n'ipsilon',\r\n'irascivel',\r\n'iris',\r\n'irlandes',\r\n'irmaos',\r\n'iroques',\r\n'irrupcao',\r\n'isca',\r\n'isento',\r\n'islandes',\r\n'isotopo',\r\n'isqueiro',\r\n'israelita',\r\n'isso',\r\n'isto',\r\n'iterbio',\r\n'itinerario',\r\n'itrio',\r\n'iuane',\r\n'iugoslavo',\r\n'jabuticabeira',\r\n'jacutinga',\r\n'jade',\r\n'jagunco',\r\n'jainista',\r\n'jaleco',\r\n'jambo',\r\n'jantarada',\r\n'japones',\r\n'jaqueta',\r\n'jarro',\r\n'jasmim',\r\n'jato',\r\n'jaula',\r\n'javel',\r\n'jazz',\r\n'jegue',\r\n'jeitoso',\r\n'jejum',\r\n'jenipapo',\r\n'jeova',\r\n'jequitiba',\r\n'jersei',\r\n'jesus',\r\n'jetom',\r\n'jiboia',\r\n'jihad',\r\n'jilo',\r\n'jingle',\r\n'jipe',\r\n'jocoso',\r\n'joelho',\r\n'joguete',\r\n'joio',\r\n'jojoba',\r\n'jorro',\r\n'jota',\r\n'joule',\r\n'joviano',\r\n'jubiloso',\r\n'judoca',\r\n'jugular',\r\n'juizo',\r\n'jujuba',\r\n'juliano',\r\n'jumento',\r\n'junto',\r\n'jururu',\r\n'justo',\r\n'juta',\r\n'juventude',\r\n'labutar',\r\n'laguna',\r\n'laico',\r\n'lajota',\r\n'lanterninha',\r\n'lapso',\r\n'laquear',\r\n'lastro',\r\n'lauto',\r\n'lavrar',\r\n'laxativo',\r\n'lazer',\r\n'leasing',\r\n'lebre',\r\n'lecionar',\r\n'ledo',\r\n'leguminoso',\r\n'leitura',\r\n'lele',\r\n'lemure',\r\n'lento',\r\n'leonardo',\r\n'leopardo',\r\n'lepton',\r\n'leque',\r\n'leste',\r\n'letreiro',\r\n'leucocito',\r\n'levitico',\r\n'lexicologo',\r\n'lhama',\r\n'lhufas',\r\n'liame',\r\n'licoroso',\r\n'lidocaina',\r\n'liliputiano',\r\n'limusine',\r\n'linotipo',\r\n'lipoproteina',\r\n'liquidos',\r\n'lirismo',\r\n'lisura',\r\n'liturgico',\r\n'livros',\r\n'lixo',\r\n'lobulo',\r\n'locutor',\r\n'lodo',\r\n'logro',\r\n'lojista',\r\n'lombriga',\r\n'lontra',\r\n'loop',\r\n'loquaz',\r\n'lorota',\r\n'losango',\r\n'lotus',\r\n'louvor',\r\n'luar',\r\n'lubrificavel',\r\n'lucros',\r\n'lugubre',\r\n'luis',\r\n'luminoso',\r\n'luneta',\r\n'lustroso',\r\n'luto',\r\n'luvas',\r\n'luxuriante',\r\n'luzeiro',\r\n'maduro',\r\n'maestro',\r\n'mafioso',\r\n'magro',\r\n'maiuscula',\r\n'majoritario',\r\n'malvisto',\r\n'mamute',\r\n'manutencao',\r\n'mapoteca',\r\n'maquinista',\r\n'marzipa',\r\n'masturbar',\r\n'matuto',\r\n'mausoleu',\r\n'mavioso',\r\n'maxixe',\r\n'mazurca',\r\n'meandro',\r\n'mecha',\r\n'medusa',\r\n'mefistofelico',\r\n'megera',\r\n'meirinho',\r\n'melro',\r\n'memorizar',\r\n'menu',\r\n'mequetrefe',\r\n'mertiolate',\r\n'mestria',\r\n'metroviario',\r\n'mexilhao',\r\n'mezanino',\r\n'miau',\r\n'microssegundo',\r\n'midia',\r\n'migratorio',\r\n'mimosa',\r\n'minuto',\r\n'miosotis',\r\n'mirtilo',\r\n'misturar',\r\n'mitzvah',\r\n'miudos',\r\n'mixuruca',\r\n'mnemonico',\r\n'moagem',\r\n'mobilizar',\r\n'modulo',\r\n'moer',\r\n'mofo',\r\n'mogno',\r\n'moita',\r\n'molusco',\r\n'monumento',\r\n'moqueca',\r\n'morubixaba',\r\n'mostruario',\r\n'motriz',\r\n'mouse',\r\n'movivel',\r\n'mozarela',\r\n'muarra',\r\n'muculmano',\r\n'mudo',\r\n'mugir',\r\n'muitos',\r\n'mumunha',\r\n'munir',\r\n'muon',\r\n'muquira',\r\n'murros',\r\n'musselina',\r\n'nacoes',\r\n'nado',\r\n'naftalina',\r\n'nago',\r\n'naipe',\r\n'naja',\r\n'nalgum',\r\n'namoro',\r\n'nanquim',\r\n'napolitano',\r\n'naquilo',\r\n'nascimento',\r\n'nautilo',\r\n'navios',\r\n'nazista',\r\n'nebuloso',\r\n'nectarina',\r\n'nefrologo',\r\n'negus',\r\n'nelore',\r\n'nenufar',\r\n'nepotismo',\r\n'nervura',\r\n'neste',\r\n'netuno',\r\n'neutron',\r\n'nevoeiro',\r\n'newtoniano',\r\n'nexo',\r\n'nhenhenhem',\r\n'nhoque',\r\n'nigeriano',\r\n'niilista',\r\n'ninho',\r\n'niobio',\r\n'niponico',\r\n'niquelar',\r\n'nirvana',\r\n'nisto',\r\n'nitroglicerina',\r\n'nivoso',\r\n'nobreza',\r\n'nocivo',\r\n'noel',\r\n'nogueira',\r\n'noivo',\r\n'nojo',\r\n'nominativo',\r\n'nonuplo',\r\n'noruegues',\r\n'nostalgico',\r\n'noturno',\r\n'nouveau',\r\n'nuanca',\r\n'nublar',\r\n'nucleotideo',\r\n'nudista',\r\n'nulo',\r\n'numismatico',\r\n'nunquinha',\r\n'nupcias',\r\n'nutritivo',\r\n'nuvens',\r\n'oasis',\r\n'obcecar',\r\n'obeso',\r\n'obituario',\r\n'objetos',\r\n'oblongo',\r\n'obnoxio',\r\n'obrigatorio',\r\n'obstruir',\r\n'obtuso',\r\n'obus',\r\n'obvio',\r\n'ocaso',\r\n'occipital',\r\n'oceanografo',\r\n'ocioso',\r\n'oclusivo',\r\n'ocorrer',\r\n'ocre',\r\n'octogono',\r\n'odalisca',\r\n'odisseia',\r\n'odorifico',\r\n'oersted',\r\n'oeste',\r\n'ofertar',\r\n'ofidio',\r\n'oftalmologo',\r\n'ogiva',\r\n'ogum',\r\n'oigale',\r\n'oitavo',\r\n'oitocentos',\r\n'ojeriza',\r\n'olaria',\r\n'oleoso',\r\n'olfato',\r\n'olhos',\r\n'oliveira',\r\n'olmo',\r\n'olor',\r\n'olvidavel',\r\n'ombudsman',\r\n'omeleteira',\r\n'omitir',\r\n'omoplata',\r\n'onanismo',\r\n'ondular',\r\n'oneroso',\r\n'onomatopeico',\r\n'ontologico',\r\n'onus',\r\n'onze',\r\n'opalescente',\r\n'opcional',\r\n'operistico',\r\n'opio',\r\n'oposto',\r\n'oprobrio',\r\n'optometrista',\r\n'opusculo',\r\n'oratorio',\r\n'orbital',\r\n'orcar',\r\n'orfao',\r\n'orixa',\r\n'orla',\r\n'ornitologo',\r\n'orquidea',\r\n'ortorrombico',\r\n'orvalho',\r\n'osculo',\r\n'osmotico',\r\n'ossudo',\r\n'ostrogodo',\r\n'otario',\r\n'otite',\r\n'ouro',\r\n'ousar',\r\n'outubro',\r\n'ouvir',\r\n'ovario',\r\n'overnight',\r\n'oviparo',\r\n'ovni',\r\n'ovoviviparo',\r\n'ovulo',\r\n'oxala',\r\n'oxente',\r\n'oxiuro',\r\n'oxossi',\r\n'ozonizar',\r\n'paciente',\r\n'pactuar',\r\n'padronizar',\r\n'paete',\r\n'pagodeiro',\r\n'paixao',\r\n'pajem',\r\n'paludismo',\r\n'pampas',\r\n'panturrilha',\r\n'papudo',\r\n'paquistanes',\r\n'pastoso',\r\n'patua',\r\n'paulo',\r\n'pauzinhos',\r\n'pavoroso',\r\n'paxa',\r\n'pazes',\r\n'peao',\r\n'pecuniario',\r\n'pedunculo',\r\n'pegaso',\r\n'peixinho',\r\n'pejorativo',\r\n'pelvis',\r\n'penuria',\r\n'pequno',\r\n'petunia',\r\n'pezada',\r\n'piauiense',\r\n'pictorico',\r\n'pierro',\r\n'pigmeu',\r\n'pijama',\r\n'pilulas',\r\n'pimpolho',\r\n'pintura',\r\n'piorar',\r\n'pipocar',\r\n'piqueteiro',\r\n'pirulito',\r\n'pistoleiro',\r\n'pituitaria',\r\n'pivotar',\r\n'pixote',\r\n'pizzaria',\r\n'plistoceno',\r\n'plotar',\r\n'pluviometrico',\r\n'pneumonico',\r\n'poco',\r\n'podridao',\r\n'poetisa',\r\n'pogrom',\r\n'pois',\r\n'polvorosa',\r\n'pomposo',\r\n'ponderado',\r\n'pontudo',\r\n'populoso',\r\n'poquer',\r\n'porvir',\r\n'posudo',\r\n'potro',\r\n'pouso',\r\n'povoar',\r\n'prazo',\r\n'prezar',\r\n'privilegios',\r\n'proximo',\r\n'prussiano',\r\n'pseudopode',\r\n'psoriase',\r\n'pterossauros',\r\n'ptialina',\r\n'ptolemaico',\r\n'pudor',\r\n'pueril',\r\n'pufe',\r\n'pugilista',\r\n'puir',\r\n'pujante',\r\n'pulverizar',\r\n'pumba',\r\n'punk',\r\n'purulento',\r\n'pustula',\r\n'putsch',\r\n'puxe',\r\n'quatrocentos',\r\n'quetzal',\r\n'quixotesco',\r\n'quotizavel',\r\n'rabujice',\r\n'racista',\r\n'radonio',\r\n'rafia',\r\n'ragu',\r\n'rajado',\r\n'ralo',\r\n'rampeiro',\r\n'ranzinza',\r\n'raptor',\r\n'raquitismo',\r\n'raro',\r\n'rasurar',\r\n'ratoeira',\r\n'ravioli',\r\n'razoavel',\r\n'reavivar',\r\n'rebuscar',\r\n'recusavel',\r\n'reduzivel',\r\n'reexposicao',\r\n'refutavel',\r\n'regurgitar',\r\n'reivindicavel',\r\n'rejuvenescimento',\r\n'relva',\r\n'remuneravel',\r\n'renunciar',\r\n'reorientar',\r\n'repuxo',\r\n'requisito',\r\n'resumo',\r\n'returno',\r\n'reutilizar',\r\n'revolvido',\r\n'rezonear',\r\n'riacho',\r\n'ribossomo',\r\n'ricota',\r\n'ridiculo',\r\n'rifle',\r\n'rigoroso',\r\n'rijo',\r\n'rimel',\r\n'rins',\r\n'rios',\r\n'riqueza',\r\n'riquixa',\r\n'rissole',\r\n'ritualistico',\r\n'rivalizar',\r\n'rixa',\r\n'robusto',\r\n'rococo',\r\n'rodoviario',\r\n'roer',\r\n'rogo',\r\n'rojao',\r\n'rolo',\r\n'rompimento',\r\n'ronronar',\r\n'roqueiro',\r\n'rorqual',\r\n'rosto',\r\n'rotundo',\r\n'rouxinol',\r\n'roxo',\r\n'royal',\r\n'ruas',\r\n'rucula',\r\n'rudimentos',\r\n'ruela',\r\n'rufo',\r\n'rugoso',\r\n'ruivo',\r\n'rule',\r\n'rumoroso',\r\n'runico',\r\n'ruptura',\r\n'rural',\r\n'rustico',\r\n'rutilar',\r\n'saariano',\r\n'sabujo',\r\n'sacudir',\r\n'sadomasoquista',\r\n'safra',\r\n'sagui',\r\n'sais',\r\n'samurai',\r\n'santuario',\r\n'sapo',\r\n'saquear',\r\n'sartriano',\r\n'saturno',\r\n'saude',\r\n'sauva',\r\n'saveiro',\r\n'saxofonista',\r\n'sazonal',\r\n'scherzo',\r\n'script',\r\n'seara',\r\n'seborreia',\r\n'secura',\r\n'seduzir',\r\n'sefardim',\r\n'seguro',\r\n'seja',\r\n'selvas',\r\n'sempre',\r\n'senzala',\r\n'sepultura',\r\n'sequoia',\r\n'sestercio',\r\n'setuplo',\r\n'seus',\r\n'seviciar',\r\n'sezonismo',\r\n'shalom',\r\n'siames',\r\n'sibilante',\r\n'sicrano',\r\n'sidra',\r\n'sifilitico',\r\n'signos',\r\n'silvo',\r\n'simultaneo',\r\n'sinusite',\r\n'sionista',\r\n'sirio',\r\n'sisudo',\r\n'situar',\r\n'sivan',\r\n'slide',\r\n'slogan',\r\n'soar',\r\n'sobrio',\r\n'socratico',\r\n'sodomizar',\r\n'soerguer',\r\n'software',\r\n'sogro',\r\n'soja',\r\n'solver',\r\n'somente',\r\n'sonso',\r\n'sopro',\r\n'soquete',\r\n'sorveteiro',\r\n'sossego',\r\n'soturno',\r\n'sousafone',\r\n'sovinice',\r\n'sozinho',\r\n'suavizar',\r\n'subverter',\r\n'sucursal',\r\n'sudoriparo',\r\n'sufragio',\r\n'sugestoes',\r\n'suite',\r\n'sujo',\r\n'sultao',\r\n'sumula',\r\n'suntuoso',\r\n'suor',\r\n'supurar',\r\n'suruba',\r\n'susto',\r\n'suturar',\r\n'suvenir',\r\n'tabuleta',\r\n'taco',\r\n'tadjique',\r\n'tafeta',\r\n'tagarelice',\r\n'taitiano',\r\n'talvez',\r\n'tampouco',\r\n'tanzaniano',\r\n'taoista',\r\n'tapume',\r\n'taquion',\r\n'tarugo',\r\n'tascar',\r\n'tatuar',\r\n'tautologico',\r\n'tavola',\r\n'taxionomista',\r\n'tchecoslovaco',\r\n'teatrologo',\r\n'tectonismo',\r\n'tedioso',\r\n'teflon',\r\n'tegumento',\r\n'teixo',\r\n'telurio',\r\n'temporas',\r\n'tenue',\r\n'teosofico',\r\n'tepido',\r\n'tequila',\r\n'terrorista',\r\n'testosterona',\r\n'tetrico',\r\n'teutonico',\r\n'teve',\r\n'texugo',\r\n'tiara',\r\n'tibia',\r\n'tiete',\r\n'tifoide',\r\n'tigresa',\r\n'tijolo',\r\n'tilintar',\r\n'timpano',\r\n'tintureiro',\r\n'tiquete',\r\n'tiroteio',\r\n'tisico',\r\n'titulos',\r\n'tive',\r\n'toar',\r\n'toboga',\r\n'tofu',\r\n'togoles',\r\n'toicinho',\r\n'tolueno',\r\n'tomografo',\r\n'tontura',\r\n'toponimo',\r\n'toquio',\r\n'torvelinho',\r\n'tostar',\r\n'toto',\r\n'touro',\r\n'toxina',\r\n'trazer',\r\n'trezentos',\r\n'trivialidade',\r\n'trovoar',\r\n'truta',\r\n'tuaregue',\r\n'tubular',\r\n'tucano',\r\n'tudo',\r\n'tufo',\r\n'tuiste',\r\n'tulipa',\r\n'tumultuoso',\r\n'tunisino',\r\n'tupiniquim',\r\n'turvo',\r\n'tutu',\r\n'ucraniano',\r\n'udenista',\r\n'ufanista',\r\n'ufologo',\r\n'ugaritico',\r\n'uiste',\r\n'uivo',\r\n'ulceroso',\r\n'ulema',\r\n'ultravioleta',\r\n'umbilical',\r\n'umero',\r\n'umido',\r\n'umlaut',\r\n'unanimidade',\r\n'unesco',\r\n'ungulado',\r\n'unheiro',\r\n'univoco',\r\n'untuoso',\r\n'urano',\r\n'urbano',\r\n'urdir',\r\n'uretra',\r\n'urgente',\r\n'urinol',\r\n'urna',\r\n'urologo',\r\n'urro',\r\n'ursulina',\r\n'urtiga',\r\n'urupe',\r\n'usavel',\r\n'usbeque',\r\n'usei',\r\n'usineiro',\r\n'usurpar',\r\n'utero',\r\n'utilizar',\r\n'utopico',\r\n'uvular',\r\n'uxoricidio',\r\n'vacuo',\r\n'vadio',\r\n'vaguear',\r\n'vaivem',\r\n'valvula',\r\n'vampiro',\r\n'vantajoso',\r\n'vaporoso',\r\n'vaquinha',\r\n'varziano',\r\n'vasto',\r\n'vaticinio',\r\n'vaudeville',\r\n'vazio',\r\n'veado',\r\n'vedico',\r\n'veemente',\r\n'vegetativo',\r\n'veio',\r\n'veja',\r\n'veludo',\r\n'venusiano',\r\n'verdade',\r\n'verve',\r\n'vestuario',\r\n'vetusto',\r\n'vexatorio',\r\n'vezes',\r\n'viavel',\r\n'vibratorio',\r\n'victor',\r\n'vicunha',\r\n'vidros',\r\n'vietnamita',\r\n'vigoroso',\r\n'vilipendiar',\r\n'vime',\r\n'vintem',\r\n'violoncelo',\r\n'viquingue',\r\n'virus',\r\n'visualizar',\r\n'vituperio',\r\n'viuvo',\r\n'vivo',\r\n'vizir',\r\n'voar',\r\n'vociferar',\r\n'vodu',\r\n'vogar',\r\n'voile',\r\n'volver',\r\n'vomito',\r\n'vontade',\r\n'vortice',\r\n'vosso',\r\n'voto',\r\n'vovozinha',\r\n'voyeuse',\r\n'vozes',\r\n'vulva',\r\n'vupt',\r\n'western',\r\n'xadrez',\r\n'xale',\r\n'xampu',\r\n'xango',\r\n'xarope',\r\n'xaual',\r\n'xavante',\r\n'xaxim',\r\n'xenonio',\r\n'xepa',\r\n'xerox',\r\n'xicara',\r\n'xifopago',\r\n'xiita',\r\n'xilogravura',\r\n'xinxim',\r\n'xistoso',\r\n'xixi',\r\n'xodo',\r\n'xogum',\r\n'xucro',\r\n'zabumba',\r\n'zagueiro',\r\n'zambiano',\r\n'zanzar',\r\n'zarpar',\r\n'zebu',\r\n'zefiro',\r\n'zeloso',\r\n'zenite',\r\n'zumbi'\r\n]\r\n"
  },
  {
    "path": "lbry/wallet/words/spanish.py",
    "content": "words = [\n'ábaco',\n'abdomen',\n'abeja',\n'abierto',\n'abogado',\n'abono',\n'aborto',\n'abrazo',\n'abrir',\n'abuelo',\n'abuso',\n'acabar',\n'academia',\n'acceso',\n'acción',\n'aceite',\n'acelga',\n'acento',\n'aceptar',\n'ácido',\n'aclarar',\n'acné',\n'acoger',\n'acoso',\n'activo',\n'acto',\n'actriz',\n'actuar',\n'acudir',\n'acuerdo',\n'acusar',\n'adicto',\n'admitir',\n'adoptar',\n'adorno',\n'aduana',\n'adulto',\n'aéreo',\n'afectar',\n'afición',\n'afinar',\n'afirmar',\n'ágil',\n'agitar',\n'agonía',\n'agosto',\n'agotar',\n'agregar',\n'agrio',\n'agua',\n'agudo',\n'águila',\n'aguja',\n'ahogo',\n'ahorro',\n'aire',\n'aislar',\n'ajedrez',\n'ajeno',\n'ajuste',\n'alacrán',\n'alambre',\n'alarma',\n'alba',\n'álbum',\n'alcalde',\n'aldea',\n'alegre',\n'alejar',\n'alerta',\n'aleta',\n'alfiler',\n'alga',\n'algodón',\n'aliado',\n'aliento',\n'alivio',\n'alma',\n'almeja',\n'almíbar',\n'altar',\n'alteza',\n'altivo',\n'alto',\n'altura',\n'alumno',\n'alzar',\n'amable',\n'amante',\n'amapola',\n'amargo',\n'amasar',\n'ámbar',\n'ámbito',\n'ameno',\n'amigo',\n'amistad',\n'amor',\n'amparo',\n'amplio',\n'ancho',\n'anciano',\n'ancla',\n'andar',\n'andén',\n'anemia',\n'ángulo',\n'anillo',\n'ánimo',\n'anís',\n'anotar',\n'antena',\n'antiguo',\n'antojo',\n'anual',\n'anular',\n'anuncio',\n'añadir',\n'añejo',\n'año',\n'apagar',\n'aparato',\n'apetito',\n'apio',\n'aplicar',\n'apodo',\n'aporte',\n'apoyo',\n'aprender',\n'aprobar',\n'apuesta',\n'apuro',\n'arado',\n'araña',\n'arar',\n'árbitro',\n'árbol',\n'arbusto',\n'archivo',\n'arco',\n'arder',\n'ardilla',\n'arduo',\n'área',\n'árido',\n'aries',\n'armonía',\n'arnés',\n'aroma',\n'arpa',\n'arpón',\n'arreglo',\n'arroz',\n'arruga',\n'arte',\n'artista',\n'asa',\n'asado',\n'asalto',\n'ascenso',\n'asegurar',\n'aseo',\n'asesor',\n'asiento',\n'asilo',\n'asistir',\n'asno',\n'asombro',\n'áspero',\n'astilla',\n'astro',\n'astuto',\n'asumir',\n'asunto',\n'atajo',\n'ataque',\n'atar',\n'atento',\n'ateo',\n'ático',\n'atleta',\n'átomo',\n'atraer',\n'atroz',\n'atún',\n'audaz',\n'audio',\n'auge',\n'aula',\n'aumento',\n'ausente',\n'autor',\n'aval',\n'avance',\n'avaro',\n'ave',\n'avellana',\n'avena',\n'avestruz',\n'avión',\n'aviso',\n'ayer',\n'ayuda',\n'ayuno',\n'azafrán',\n'azar',\n'azote',\n'azúcar',\n'azufre',\n'azul',\n'baba',\n'babor',\n'bache',\n'bahía',\n'baile',\n'bajar',\n'balanza',\n'balcón',\n'balde',\n'bambú',\n'banco',\n'banda',\n'baño',\n'barba',\n'barco',\n'barniz',\n'barro',\n'báscula',\n'bastón',\n'basura',\n'batalla',\n'batería',\n'batir',\n'batuta',\n'baúl',\n'bazar',\n'bebé',\n'bebida',\n'bello',\n'besar',\n'beso',\n'bestia',\n'bicho',\n'bien',\n'bingo',\n'blanco',\n'bloque',\n'blusa',\n'boa',\n'bobina',\n'bobo',\n'boca',\n'bocina',\n'boda',\n'bodega',\n'boina',\n'bola',\n'bolero',\n'bolsa',\n'bomba',\n'bondad',\n'bonito',\n'bono',\n'bonsái',\n'borde',\n'borrar',\n'bosque',\n'bote',\n'botín',\n'bóveda',\n'bozal',\n'bravo',\n'brazo',\n'brecha',\n'breve',\n'brillo',\n'brinco',\n'brisa',\n'broca',\n'broma',\n'bronce',\n'brote',\n'bruja',\n'brusco',\n'bruto',\n'buceo',\n'bucle',\n'bueno',\n'buey',\n'bufanda',\n'bufón',\n'búho',\n'buitre',\n'bulto',\n'burbuja',\n'burla',\n'burro',\n'buscar',\n'butaca',\n'buzón',\n'caballo',\n'cabeza',\n'cabina',\n'cabra',\n'cacao',\n'cadáver',\n'cadena',\n'caer',\n'café',\n'caída',\n'caimán',\n'caja',\n'cajón',\n'cal',\n'calamar',\n'calcio',\n'caldo',\n'calidad',\n'calle',\n'calma',\n'calor',\n'calvo',\n'cama',\n'cambio',\n'camello',\n'camino',\n'campo',\n'cáncer',\n'candil',\n'canela',\n'canguro',\n'canica',\n'canto',\n'caña',\n'cañón',\n'caoba',\n'caos',\n'capaz',\n'capitán',\n'capote',\n'captar',\n'capucha',\n'cara',\n'carbón',\n'cárcel',\n'careta',\n'carga',\n'cariño',\n'carne',\n'carpeta',\n'carro',\n'carta',\n'casa',\n'casco',\n'casero',\n'caspa',\n'castor',\n'catorce',\n'catre',\n'caudal',\n'causa',\n'cazo',\n'cebolla',\n'ceder',\n'cedro',\n'celda',\n'célebre',\n'celoso',\n'célula',\n'cemento',\n'ceniza',\n'centro',\n'cerca',\n'cerdo',\n'cereza',\n'cero',\n'cerrar',\n'certeza',\n'césped',\n'cetro',\n'chacal',\n'chaleco',\n'champú',\n'chancla',\n'chapa',\n'charla',\n'chico',\n'chiste',\n'chivo',\n'choque',\n'choza',\n'chuleta',\n'chupar',\n'ciclón',\n'ciego',\n'cielo',\n'cien',\n'cierto',\n'cifra',\n'cigarro',\n'cima',\n'cinco',\n'cine',\n'cinta',\n'ciprés',\n'circo',\n'ciruela',\n'cisne',\n'cita',\n'ciudad',\n'clamor',\n'clan',\n'claro',\n'clase',\n'clave',\n'cliente',\n'clima',\n'clínica',\n'cobre',\n'cocción',\n'cochino',\n'cocina',\n'coco',\n'código',\n'codo',\n'cofre',\n'coger',\n'cohete',\n'cojín',\n'cojo',\n'cola',\n'colcha',\n'colegio',\n'colgar',\n'colina',\n'collar',\n'colmo',\n'columna',\n'combate',\n'comer',\n'comida',\n'cómodo',\n'compra',\n'conde',\n'conejo',\n'conga',\n'conocer',\n'consejo',\n'contar',\n'copa',\n'copia',\n'corazón',\n'corbata',\n'corcho',\n'cordón',\n'corona',\n'correr',\n'coser',\n'cosmos',\n'costa',\n'cráneo',\n'cráter',\n'crear',\n'crecer',\n'creído',\n'crema',\n'cría',\n'crimen',\n'cripta',\n'crisis',\n'cromo',\n'crónica',\n'croqueta',\n'crudo',\n'cruz',\n'cuadro',\n'cuarto',\n'cuatro',\n'cubo',\n'cubrir',\n'cuchara',\n'cuello',\n'cuento',\n'cuerda',\n'cuesta',\n'cueva',\n'cuidar',\n'culebra',\n'culpa',\n'culto',\n'cumbre',\n'cumplir',\n'cuna',\n'cuneta',\n'cuota',\n'cupón',\n'cúpula',\n'curar',\n'curioso',\n'curso',\n'curva',\n'cutis',\n'dama',\n'danza',\n'dar',\n'dardo',\n'dátil',\n'deber',\n'débil',\n'década',\n'decir',\n'dedo',\n'defensa',\n'definir',\n'dejar',\n'delfín',\n'delgado',\n'delito',\n'demora',\n'denso',\n'dental',\n'deporte',\n'derecho',\n'derrota',\n'desayuno',\n'deseo',\n'desfile',\n'desnudo',\n'destino',\n'desvío',\n'detalle',\n'detener',\n'deuda',\n'día',\n'diablo',\n'diadema',\n'diamante',\n'diana',\n'diario',\n'dibujo',\n'dictar',\n'diente',\n'dieta',\n'diez',\n'difícil',\n'digno',\n'dilema',\n'diluir',\n'dinero',\n'directo',\n'dirigir',\n'disco',\n'diseño',\n'disfraz',\n'diva',\n'divino',\n'doble',\n'doce',\n'dolor',\n'domingo',\n'don',\n'donar',\n'dorado',\n'dormir',\n'dorso',\n'dos',\n'dosis',\n'dragón',\n'droga',\n'ducha',\n'duda',\n'duelo',\n'dueño',\n'dulce',\n'dúo',\n'duque',\n'durar',\n'dureza',\n'duro',\n'ébano',\n'ebrio',\n'echar',\n'eco',\n'ecuador',\n'edad',\n'edición',\n'edificio',\n'editor',\n'educar',\n'efecto',\n'eficaz',\n'eje',\n'ejemplo',\n'elefante',\n'elegir',\n'elemento',\n'elevar',\n'elipse',\n'élite',\n'elixir',\n'elogio',\n'eludir',\n'embudo',\n'emitir',\n'emoción',\n'empate',\n'empeño',\n'empleo',\n'empresa',\n'enano',\n'encargo',\n'enchufe',\n'encía',\n'enemigo',\n'enero',\n'enfado',\n'enfermo',\n'engaño',\n'enigma',\n'enlace',\n'enorme',\n'enredo',\n'ensayo',\n'enseñar',\n'entero',\n'entrar',\n'envase',\n'envío',\n'época',\n'equipo',\n'erizo',\n'escala',\n'escena',\n'escolar',\n'escribir',\n'escudo',\n'esencia',\n'esfera',\n'esfuerzo',\n'espada',\n'espejo',\n'espía',\n'esposa',\n'espuma',\n'esquí',\n'estar',\n'este',\n'estilo',\n'estufa',\n'etapa',\n'eterno',\n'ética',\n'etnia',\n'evadir',\n'evaluar',\n'evento',\n'evitar',\n'exacto',\n'examen',\n'exceso',\n'excusa',\n'exento',\n'exigir',\n'exilio',\n'existir',\n'éxito',\n'experto',\n'explicar',\n'exponer',\n'extremo',\n'fábrica',\n'fábula',\n'fachada',\n'fácil',\n'factor',\n'faena',\n'faja',\n'falda',\n'fallo',\n'falso',\n'faltar',\n'fama',\n'familia',\n'famoso',\n'faraón',\n'farmacia',\n'farol',\n'farsa',\n'fase',\n'fatiga',\n'fauna',\n'favor',\n'fax',\n'febrero',\n'fecha',\n'feliz',\n'feo',\n'feria',\n'feroz',\n'fértil',\n'fervor',\n'festín',\n'fiable',\n'fianza',\n'fiar',\n'fibra',\n'ficción',\n'ficha',\n'fideo',\n'fiebre',\n'fiel',\n'fiera',\n'fiesta',\n'figura',\n'fijar',\n'fijo',\n'fila',\n'filete',\n'filial',\n'filtro',\n'fin',\n'finca',\n'fingir',\n'finito',\n'firma',\n'flaco',\n'flauta',\n'flecha',\n'flor',\n'flota',\n'fluir',\n'flujo',\n'flúor',\n'fobia',\n'foca',\n'fogata',\n'fogón',\n'folio',\n'folleto',\n'fondo',\n'forma',\n'forro',\n'fortuna',\n'forzar',\n'fosa',\n'foto',\n'fracaso',\n'frágil',\n'franja',\n'frase',\n'fraude',\n'freír',\n'freno',\n'fresa',\n'frío',\n'frito',\n'fruta',\n'fuego',\n'fuente',\n'fuerza',\n'fuga',\n'fumar',\n'función',\n'funda',\n'furgón',\n'furia',\n'fusil',\n'fútbol',\n'futuro',\n'gacela',\n'gafas',\n'gaita',\n'gajo',\n'gala',\n'galería',\n'gallo',\n'gamba',\n'ganar',\n'gancho',\n'ganga',\n'ganso',\n'garaje',\n'garza',\n'gasolina',\n'gastar',\n'gato',\n'gavilán',\n'gemelo',\n'gemir',\n'gen',\n'género',\n'genio',\n'gente',\n'geranio',\n'gerente',\n'germen',\n'gesto',\n'gigante',\n'gimnasio',\n'girar',\n'giro',\n'glaciar',\n'globo',\n'gloria',\n'gol',\n'golfo',\n'goloso',\n'golpe',\n'goma',\n'gordo',\n'gorila',\n'gorra',\n'gota',\n'goteo',\n'gozar',\n'grada',\n'gráfico',\n'grano',\n'grasa',\n'gratis',\n'grave',\n'grieta',\n'grillo',\n'gripe',\n'gris',\n'grito',\n'grosor',\n'grúa',\n'grueso',\n'grumo',\n'grupo',\n'guante',\n'guapo',\n'guardia',\n'guerra',\n'guía',\n'guiño',\n'guion',\n'guiso',\n'guitarra',\n'gusano',\n'gustar',\n'haber',\n'hábil',\n'hablar',\n'hacer',\n'hacha',\n'hada',\n'hallar',\n'hamaca',\n'harina',\n'haz',\n'hazaña',\n'hebilla',\n'hebra',\n'hecho',\n'helado',\n'helio',\n'hembra',\n'herir',\n'hermano',\n'héroe',\n'hervir',\n'hielo',\n'hierro',\n'hígado',\n'higiene',\n'hijo',\n'himno',\n'historia',\n'hocico',\n'hogar',\n'hoguera',\n'hoja',\n'hombre',\n'hongo',\n'honor',\n'honra',\n'hora',\n'hormiga',\n'horno',\n'hostil',\n'hoyo',\n'hueco',\n'huelga',\n'huerta',\n'hueso',\n'huevo',\n'huida',\n'huir',\n'humano',\n'húmedo',\n'humilde',\n'humo',\n'hundir',\n'huracán',\n'hurto',\n'icono',\n'ideal',\n'idioma',\n'ídolo',\n'iglesia',\n'iglú',\n'igual',\n'ilegal',\n'ilusión',\n'imagen',\n'imán',\n'imitar',\n'impar',\n'imperio',\n'imponer',\n'impulso',\n'incapaz',\n'índice',\n'inerte',\n'infiel',\n'informe',\n'ingenio',\n'inicio',\n'inmenso',\n'inmune',\n'innato',\n'insecto',\n'instante',\n'interés',\n'íntimo',\n'intuir',\n'inútil',\n'invierno',\n'ira',\n'iris',\n'ironía',\n'isla',\n'islote',\n'jabalí',\n'jabón',\n'jamón',\n'jarabe',\n'jardín',\n'jarra',\n'jaula',\n'jazmín',\n'jefe',\n'jeringa',\n'jinete',\n'jornada',\n'joroba',\n'joven',\n'joya',\n'juerga',\n'jueves',\n'juez',\n'jugador',\n'jugo',\n'juguete',\n'juicio',\n'junco',\n'jungla',\n'junio',\n'juntar',\n'júpiter',\n'jurar',\n'justo',\n'juvenil',\n'juzgar',\n'kilo',\n'koala',\n'labio',\n'lacio',\n'lacra',\n'lado',\n'ladrón',\n'lagarto',\n'lágrima',\n'laguna',\n'laico',\n'lamer',\n'lámina',\n'lámpara',\n'lana',\n'lancha',\n'langosta',\n'lanza',\n'lápiz',\n'largo',\n'larva',\n'lástima',\n'lata',\n'látex',\n'latir',\n'laurel',\n'lavar',\n'lazo',\n'leal',\n'lección',\n'leche',\n'lector',\n'leer',\n'legión',\n'legumbre',\n'lejano',\n'lengua',\n'lento',\n'leña',\n'león',\n'leopardo',\n'lesión',\n'letal',\n'letra',\n'leve',\n'leyenda',\n'libertad',\n'libro',\n'licor',\n'líder',\n'lidiar',\n'lienzo',\n'liga',\n'ligero',\n'lima',\n'límite',\n'limón',\n'limpio',\n'lince',\n'lindo',\n'línea',\n'lingote',\n'lino',\n'linterna',\n'líquido',\n'liso',\n'lista',\n'litera',\n'litio',\n'litro',\n'llaga',\n'llama',\n'llanto',\n'llave',\n'llegar',\n'llenar',\n'llevar',\n'llorar',\n'llover',\n'lluvia',\n'lobo',\n'loción',\n'loco',\n'locura',\n'lógica',\n'logro',\n'lombriz',\n'lomo',\n'lonja',\n'lote',\n'lucha',\n'lucir',\n'lugar',\n'lujo',\n'luna',\n'lunes',\n'lupa',\n'lustro',\n'luto',\n'luz',\n'maceta',\n'macho',\n'madera',\n'madre',\n'maduro',\n'maestro',\n'mafia',\n'magia',\n'mago',\n'maíz',\n'maldad',\n'maleta',\n'malla',\n'malo',\n'mamá',\n'mambo',\n'mamut',\n'manco',\n'mando',\n'manejar',\n'manga',\n'maniquí',\n'manjar',\n'mano',\n'manso',\n'manta',\n'mañana',\n'mapa',\n'máquina',\n'mar',\n'marco',\n'marea',\n'marfil',\n'margen',\n'marido',\n'mármol',\n'marrón',\n'martes',\n'marzo',\n'masa',\n'máscara',\n'masivo',\n'matar',\n'materia',\n'matiz',\n'matriz',\n'máximo',\n'mayor',\n'mazorca',\n'mecha',\n'medalla',\n'medio',\n'médula',\n'mejilla',\n'mejor',\n'melena',\n'melón',\n'memoria',\n'menor',\n'mensaje',\n'mente',\n'menú',\n'mercado',\n'merengue',\n'mérito',\n'mes',\n'mesón',\n'meta',\n'meter',\n'método',\n'metro',\n'mezcla',\n'miedo',\n'miel',\n'miembro',\n'miga',\n'mil',\n'milagro',\n'militar',\n'millón',\n'mimo',\n'mina',\n'minero',\n'mínimo',\n'minuto',\n'miope',\n'mirar',\n'misa',\n'miseria',\n'misil',\n'mismo',\n'mitad',\n'mito',\n'mochila',\n'moción',\n'moda',\n'modelo',\n'moho',\n'mojar',\n'molde',\n'moler',\n'molino',\n'momento',\n'momia',\n'monarca',\n'moneda',\n'monja',\n'monto',\n'moño',\n'morada',\n'morder',\n'moreno',\n'morir',\n'morro',\n'morsa',\n'mortal',\n'mosca',\n'mostrar',\n'motivo',\n'mover',\n'móvil',\n'mozo',\n'mucho',\n'mudar',\n'mueble',\n'muela',\n'muerte',\n'muestra',\n'mugre',\n'mujer',\n'mula',\n'muleta',\n'multa',\n'mundo',\n'muñeca',\n'mural',\n'muro',\n'músculo',\n'museo',\n'musgo',\n'música',\n'muslo',\n'nácar',\n'nación',\n'nadar',\n'naipe',\n'naranja',\n'nariz',\n'narrar',\n'nasal',\n'natal',\n'nativo',\n'natural',\n'náusea',\n'naval',\n'nave',\n'navidad',\n'necio',\n'néctar',\n'negar',\n'negocio',\n'negro',\n'neón',\n'nervio',\n'neto',\n'neutro',\n'nevar',\n'nevera',\n'nicho',\n'nido',\n'niebla',\n'nieto',\n'niñez',\n'niño',\n'nítido',\n'nivel',\n'nobleza',\n'noche',\n'nómina',\n'noria',\n'norma',\n'norte',\n'nota',\n'noticia',\n'novato',\n'novela',\n'novio',\n'nube',\n'nuca',\n'núcleo',\n'nudillo',\n'nudo',\n'nuera',\n'nueve',\n'nuez',\n'nulo',\n'número',\n'nutria',\n'oasis',\n'obeso',\n'obispo',\n'objeto',\n'obra',\n'obrero',\n'observar',\n'obtener',\n'obvio',\n'oca',\n'ocaso',\n'océano',\n'ochenta',\n'ocho',\n'ocio',\n'ocre',\n'octavo',\n'octubre',\n'oculto',\n'ocupar',\n'ocurrir',\n'odiar',\n'odio',\n'odisea',\n'oeste',\n'ofensa',\n'oferta',\n'oficio',\n'ofrecer',\n'ogro',\n'oído',\n'oír',\n'ojo',\n'ola',\n'oleada',\n'olfato',\n'olivo',\n'olla',\n'olmo',\n'olor',\n'olvido',\n'ombligo',\n'onda',\n'onza',\n'opaco',\n'opción',\n'ópera',\n'opinar',\n'oponer',\n'optar',\n'óptica',\n'opuesto',\n'oración',\n'orador',\n'oral',\n'órbita',\n'orca',\n'orden',\n'oreja',\n'órgano',\n'orgía',\n'orgullo',\n'oriente',\n'origen',\n'orilla',\n'oro',\n'orquesta',\n'oruga',\n'osadía',\n'oscuro',\n'osezno',\n'oso',\n'ostra',\n'otoño',\n'otro',\n'oveja',\n'óvulo',\n'óxido',\n'oxígeno',\n'oyente',\n'ozono',\n'pacto',\n'padre',\n'paella',\n'página',\n'pago',\n'país',\n'pájaro',\n'palabra',\n'palco',\n'paleta',\n'pálido',\n'palma',\n'paloma',\n'palpar',\n'pan',\n'panal',\n'pánico',\n'pantera',\n'pañuelo',\n'papá',\n'papel',\n'papilla',\n'paquete',\n'parar',\n'parcela',\n'pared',\n'parir',\n'paro',\n'párpado',\n'parque',\n'párrafo',\n'parte',\n'pasar',\n'paseo',\n'pasión',\n'paso',\n'pasta',\n'pata',\n'patio',\n'patria',\n'pausa',\n'pauta',\n'pavo',\n'payaso',\n'peatón',\n'pecado',\n'pecera',\n'pecho',\n'pedal',\n'pedir',\n'pegar',\n'peine',\n'pelar',\n'peldaño',\n'pelea',\n'peligro',\n'pellejo',\n'pelo',\n'peluca',\n'pena',\n'pensar',\n'peñón',\n'peón',\n'peor',\n'pepino',\n'pequeño',\n'pera',\n'percha',\n'perder',\n'pereza',\n'perfil',\n'perico',\n'perla',\n'permiso',\n'perro',\n'persona',\n'pesa',\n'pesca',\n'pésimo',\n'pestaña',\n'pétalo',\n'petróleo',\n'pez',\n'pezuña',\n'picar',\n'pichón',\n'pie',\n'piedra',\n'pierna',\n'pieza',\n'pijama',\n'pilar',\n'piloto',\n'pimienta',\n'pino',\n'pintor',\n'pinza',\n'piña',\n'piojo',\n'pipa',\n'pirata',\n'pisar',\n'piscina',\n'piso',\n'pista',\n'pitón',\n'pizca',\n'placa',\n'plan',\n'plata',\n'playa',\n'plaza',\n'pleito',\n'pleno',\n'plomo',\n'pluma',\n'plural',\n'pobre',\n'poco',\n'poder',\n'podio',\n'poema',\n'poesía',\n'poeta',\n'polen',\n'policía',\n'pollo',\n'polvo',\n'pomada',\n'pomelo',\n'pomo',\n'pompa',\n'poner',\n'porción',\n'portal',\n'posada',\n'poseer',\n'posible',\n'poste',\n'potencia',\n'potro',\n'pozo',\n'prado',\n'precoz',\n'pregunta',\n'premio',\n'prensa',\n'preso',\n'previo',\n'primo',\n'príncipe',\n'prisión',\n'privar',\n'proa',\n'probar',\n'proceso',\n'producto',\n'proeza',\n'profesor',\n'programa',\n'prole',\n'promesa',\n'pronto',\n'propio',\n'próximo',\n'prueba',\n'público',\n'puchero',\n'pudor',\n'pueblo',\n'puerta',\n'puesto',\n'pulga',\n'pulir',\n'pulmón',\n'pulpo',\n'pulso',\n'puma',\n'punto',\n'puñal',\n'puño',\n'pupa',\n'pupila',\n'puré',\n'quedar',\n'queja',\n'quemar',\n'querer',\n'queso',\n'quieto',\n'química',\n'quince',\n'quitar',\n'rábano',\n'rabia',\n'rabo',\n'ración',\n'radical',\n'raíz',\n'rama',\n'rampa',\n'rancho',\n'rango',\n'rapaz',\n'rápido',\n'rapto',\n'rasgo',\n'raspa',\n'rato',\n'rayo',\n'raza',\n'razón',\n'reacción',\n'realidad',\n'rebaño',\n'rebote',\n'recaer',\n'receta',\n'rechazo',\n'recoger',\n'recreo',\n'recto',\n'recurso',\n'red',\n'redondo',\n'reducir',\n'reflejo',\n'reforma',\n'refrán',\n'refugio',\n'regalo',\n'regir',\n'regla',\n'regreso',\n'rehén',\n'reino',\n'reír',\n'reja',\n'relato',\n'relevo',\n'relieve',\n'relleno',\n'reloj',\n'remar',\n'remedio',\n'remo',\n'rencor',\n'rendir',\n'renta',\n'reparto',\n'repetir',\n'reposo',\n'reptil',\n'res',\n'rescate',\n'resina',\n'respeto',\n'resto',\n'resumen',\n'retiro',\n'retorno',\n'retrato',\n'reunir',\n'revés',\n'revista',\n'rey',\n'rezar',\n'rico',\n'riego',\n'rienda',\n'riesgo',\n'rifa',\n'rígido',\n'rigor',\n'rincón',\n'riñón',\n'río',\n'riqueza',\n'risa',\n'ritmo',\n'rito',\n'rizo',\n'roble',\n'roce',\n'rociar',\n'rodar',\n'rodeo',\n'rodilla',\n'roer',\n'rojizo',\n'rojo',\n'romero',\n'romper',\n'ron',\n'ronco',\n'ronda',\n'ropa',\n'ropero',\n'rosa',\n'rosca',\n'rostro',\n'rotar',\n'rubí',\n'rubor',\n'rudo',\n'rueda',\n'rugir',\n'ruido',\n'ruina',\n'ruleta',\n'rulo',\n'rumbo',\n'rumor',\n'ruptura',\n'ruta',\n'rutina',\n'sábado',\n'saber',\n'sabio',\n'sable',\n'sacar',\n'sagaz',\n'sagrado',\n'sala',\n'saldo',\n'salero',\n'salir',\n'salmón',\n'salón',\n'salsa',\n'salto',\n'salud',\n'salvar',\n'samba',\n'sanción',\n'sandía',\n'sanear',\n'sangre',\n'sanidad',\n'sano',\n'santo',\n'sapo',\n'saque',\n'sardina',\n'sartén',\n'sastre',\n'satán',\n'sauna',\n'saxofón',\n'sección',\n'seco',\n'secreto',\n'secta',\n'sed',\n'seguir',\n'seis',\n'sello',\n'selva',\n'semana',\n'semilla',\n'senda',\n'sensor',\n'señal',\n'señor',\n'separar',\n'sepia',\n'sequía',\n'ser',\n'serie',\n'sermón',\n'servir',\n'sesenta',\n'sesión',\n'seta',\n'setenta',\n'severo',\n'sexo',\n'sexto',\n'sidra',\n'siesta',\n'siete',\n'siglo',\n'signo',\n'sílaba',\n'silbar',\n'silencio',\n'silla',\n'símbolo',\n'simio',\n'sirena',\n'sistema',\n'sitio',\n'situar',\n'sobre',\n'socio',\n'sodio',\n'sol',\n'solapa',\n'soldado',\n'soledad',\n'sólido',\n'soltar',\n'solución',\n'sombra',\n'sondeo',\n'sonido',\n'sonoro',\n'sonrisa',\n'sopa',\n'soplar',\n'soporte',\n'sordo',\n'sorpresa',\n'sorteo',\n'sostén',\n'sótano',\n'suave',\n'subir',\n'suceso',\n'sudor',\n'suegra',\n'suelo',\n'sueño',\n'suerte',\n'sufrir',\n'sujeto',\n'sultán',\n'sumar',\n'superar',\n'suplir',\n'suponer',\n'supremo',\n'sur',\n'surco',\n'sureño',\n'surgir',\n'susto',\n'sutil',\n'tabaco',\n'tabique',\n'tabla',\n'tabú',\n'taco',\n'tacto',\n'tajo',\n'talar',\n'talco',\n'talento',\n'talla',\n'talón',\n'tamaño',\n'tambor',\n'tango',\n'tanque',\n'tapa',\n'tapete',\n'tapia',\n'tapón',\n'taquilla',\n'tarde',\n'tarea',\n'tarifa',\n'tarjeta',\n'tarot',\n'tarro',\n'tarta',\n'tatuaje',\n'tauro',\n'taza',\n'tazón',\n'teatro',\n'techo',\n'tecla',\n'técnica',\n'tejado',\n'tejer',\n'tejido',\n'tela',\n'teléfono',\n'tema',\n'temor',\n'templo',\n'tenaz',\n'tender',\n'tener',\n'tenis',\n'tenso',\n'teoría',\n'terapia',\n'terco',\n'término',\n'ternura',\n'terror',\n'tesis',\n'tesoro',\n'testigo',\n'tetera',\n'texto',\n'tez',\n'tibio',\n'tiburón',\n'tiempo',\n'tienda',\n'tierra',\n'tieso',\n'tigre',\n'tijera',\n'tilde',\n'timbre',\n'tímido',\n'timo',\n'tinta',\n'tío',\n'típico',\n'tipo',\n'tira',\n'tirón',\n'titán',\n'títere',\n'título',\n'tiza',\n'toalla',\n'tobillo',\n'tocar',\n'tocino',\n'todo',\n'toga',\n'toldo',\n'tomar',\n'tono',\n'tonto',\n'topar',\n'tope',\n'toque',\n'tórax',\n'torero',\n'tormenta',\n'torneo',\n'toro',\n'torpedo',\n'torre',\n'torso',\n'tortuga',\n'tos',\n'tosco',\n'toser',\n'tóxico',\n'trabajo',\n'tractor',\n'traer',\n'tráfico',\n'trago',\n'traje',\n'tramo',\n'trance',\n'trato',\n'trauma',\n'trazar',\n'trébol',\n'tregua',\n'treinta',\n'tren',\n'trepar',\n'tres',\n'tribu',\n'trigo',\n'tripa',\n'triste',\n'triunfo',\n'trofeo',\n'trompa',\n'tronco',\n'tropa',\n'trote',\n'trozo',\n'truco',\n'trueno',\n'trufa',\n'tubería',\n'tubo',\n'tuerto',\n'tumba',\n'tumor',\n'túnel',\n'túnica',\n'turbina',\n'turismo',\n'turno',\n'tutor',\n'ubicar',\n'úlcera',\n'umbral',\n'unidad',\n'unir',\n'universo',\n'uno',\n'untar',\n'uña',\n'urbano',\n'urbe',\n'urgente',\n'urna',\n'usar',\n'usuario',\n'útil',\n'utopía',\n'uva',\n'vaca',\n'vacío',\n'vacuna',\n'vagar',\n'vago',\n'vaina',\n'vajilla',\n'vale',\n'válido',\n'valle',\n'valor',\n'válvula',\n'vampiro',\n'vara',\n'variar',\n'varón',\n'vaso',\n'vecino',\n'vector',\n'vehículo',\n'veinte',\n'vejez',\n'vela',\n'velero',\n'veloz',\n'vena',\n'vencer',\n'venda',\n'veneno',\n'vengar',\n'venir',\n'venta',\n'venus',\n'ver',\n'verano',\n'verbo',\n'verde',\n'vereda',\n'verja',\n'verso',\n'verter',\n'vía',\n'viaje',\n'vibrar',\n'vicio',\n'víctima',\n'vida',\n'vídeo',\n'vidrio',\n'viejo',\n'viernes',\n'vigor',\n'vil',\n'villa',\n'vinagre',\n'vino',\n'viñedo',\n'violín',\n'viral',\n'virgo',\n'virtud',\n'visor',\n'víspera',\n'vista',\n'vitamina',\n'viudo',\n'vivaz',\n'vivero',\n'vivir',\n'vivo',\n'volcán',\n'volumen',\n'volver',\n'voraz',\n'votar',\n'voto',\n'voz',\n'vuelo',\n'vulgar',\n'yacer',\n'yate',\n'yegua',\n'yema',\n'yerno',\n'yeso',\n'yodo',\n'yoga',\n'yogur',\n'zafiro',\n'zanja',\n'zapato',\n'zarza',\n'zona',\n'zorro',\n'zumo',\n'zurdo'\n]\n"
  },
  {
    "path": "lbry/winpaths.py",
    "content": "# Copyright (c) 2014 Michael Kropat\n\nimport sys\nimport ctypes\nfrom ctypes import windll, wintypes\nfrom uuid import UUID\n\n# http://msdn.microsoft.com/en-us/library/windows/desktop/aa373931.aspx\nclass GUID(ctypes.Structure):\n    _fields_ = [\n        (\"Data1\", wintypes.DWORD),\n        (\"Data2\", wintypes.WORD),\n        (\"Data3\", wintypes.WORD),\n        (\"Data4\", wintypes.BYTE * 8)\n    ]\n\n    def __init__(self, uuid_):\n        super().__init__()\n        self.Data1, self.Data2, self.Data3, self.Data4[0], self.Data4[1], rest = uuid_.fields\n        for i in range(2, 8):\n            self.Data4[i] = rest>>(8 - i - 1)*8 & 0xff\n\n\n# http://msdn.microsoft.com/en-us/library/windows/desktop/dd378457.aspx\nclass FOLDERID:\n    # pylint: disable=bad-whitespace\n    AccountPictures         = UUID('{008ca0b1-55b4-4c56-b8a8-4de4b299d3be}')\n    AdminTools              = UUID('{724EF170-A42D-4FEF-9F26-B60E846FBA4F}')\n    ApplicationShortcuts    = UUID('{A3918781-E5F2-4890-B3D9-A7E54332328C}')\n    CameraRoll              = UUID('{AB5FB87B-7CE2-4F83-915D-550846C9537B}')\n    CDBurning               = UUID('{9E52AB10-F80D-49DF-ACB8-4330F5687855}')\n    CommonAdminTools        = UUID('{D0384E7D-BAC3-4797-8F14-CBA229B392B5}')\n    CommonOEMLinks          = UUID('{C1BAE2D0-10DF-4334-BEDD-7AA20B227A9D}')\n    CommonPrograms          = UUID('{0139D44E-6AFE-49F2-8690-3DAFCAE6FFB8}')\n    CommonStartMenu         = UUID('{A4115719-D62E-491D-AA7C-E74B8BE3B067}')\n    CommonStartup           = UUID('{82A5EA35-D9CD-47C5-9629-E15D2F714E6E}')\n    CommonTemplates         = UUID('{B94237E7-57AC-4347-9151-B08C6C32D1F7}')\n    Contacts                = UUID('{56784854-C6CB-462b-8169-88E350ACB882}')\n    Cookies                 = UUID('{2B0F765D-C0E9-4171-908E-08A611B84FF6}')\n    Desktop                 = UUID('{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}')\n    DeviceMetadataStore     = UUID('{5CE4A5E9-E4EB-479D-B89F-130C02886155}')\n    Documents               = UUID('{FDD39AD0-238F-46AF-ADB4-6C85480369C7}')\n    DocumentsLibrary        = UUID('{7B0DB17D-9CD2-4A93-9733-46CC89022E7C}')\n    Downloads               = UUID('{374DE290-123F-4565-9164-39C4925E467B}')\n    Favorites               = UUID('{1777F761-68AD-4D8A-87BD-30B759FA33DD}')\n    Fonts                   = UUID('{FD228CB7-AE11-4AE3-864C-16F3910AB8FE}')\n    GameTasks               = UUID('{054FAE61-4DD8-4787-80B6-090220C4B700}')\n    History                 = UUID('{D9DC8A3B-B784-432E-A781-5A1130A75963}')\n    ImplicitAppShortcuts    = UUID('{BCB5256F-79F6-4CEE-B725-DC34E402FD46}')\n    InternetCache           = UUID('{352481E8-33BE-4251-BA85-6007CAEDCF9D}')\n    Libraries               = UUID('{1B3EA5DC-B587-4786-B4EF-BD1DC332AEAE}')\n    Links                   = UUID('{bfb9d5e0-c6a9-404c-b2b2-ae6db6af4968}')\n    LocalAppData            = UUID('{F1B32785-6FBA-4FCF-9D55-7B8E7F157091}')\n    LocalAppDataLow         = UUID('{A520A1A4-1780-4FF6-BD18-167343C5AF16}')\n    LocalizedResourcesDir   = UUID('{2A00375E-224C-49DE-B8D1-440DF7EF3DDC}')\n    Music                   = UUID('{4BD8D571-6D19-48D3-BE97-422220080E43}')\n    MusicLibrary            = UUID('{2112AB0A-C86A-4FFE-A368-0DE96E47012E}')\n    NetHood                 = UUID('{C5ABBF53-E17F-4121-8900-86626FC2C973}')\n    OriginalImages          = UUID('{2C36C0AA-5812-4b87-BFD0-4CD0DFB19B39}')\n    PhotoAlbums             = UUID('{69D2CF90-FC33-4FB7-9A0C-EBB0F0FCB43C}')\n    PicturesLibrary         = UUID('{A990AE9F-A03B-4E80-94BC-9912D7504104}')\n    Pictures                = UUID('{33E28130-4E1E-4676-835A-98395C3BC3BB}')\n    Playlists               = UUID('{DE92C1C7-837F-4F69-A3BB-86E631204A23}')\n    PrintHood               = UUID('{9274BD8D-CFD1-41C3-B35E-B13F55A758F4}')\n    Profile                 = UUID('{5E6C858F-0E22-4760-9AFE-EA3317B67173}')\n    ProgramData             = UUID('{62AB5D82-FDC1-4DC3-A9DD-070D1D495D97}')\n    ProgramFiles            = UUID('{905e63b6-c1bf-494e-b29c-65b732d3d21a}')\n    ProgramFilesX64         = UUID('{6D809377-6AF0-444b-8957-A3773F02200E}')\n    ProgramFilesX86         = UUID('{7C5A40EF-A0FB-4BFC-874A-C0F2E0B9FA8E}')\n    ProgramFilesCommon      = UUID('{F7F1ED05-9F6D-47A2-AAAE-29D317C6F066}')\n    ProgramFilesCommonX64   = UUID('{6365D5A7-0F0D-45E5-87F6-0DA56B6A4F7D}')\n    ProgramFilesCommonX86   = UUID('{DE974D24-D9C6-4D3E-BF91-F4455120B917}')\n    Programs                = UUID('{A77F5D77-2E2B-44C3-A6A2-ABA601054A51}')\n    Public                  = UUID('{DFDF76A2-C82A-4D63-906A-5644AC457385}')\n    PublicDesktop           = UUID('{C4AA340D-F20F-4863-AFEF-F87EF2E6BA25}')\n    PublicDocuments         = UUID('{ED4824AF-DCE4-45A8-81E2-FC7965083634}')\n    PublicDownloads         = UUID('{3D644C9B-1FB8-4f30-9B45-F670235F79C0}')\n    PublicGameTasks         = UUID('{DEBF2536-E1A8-4c59-B6A2-414586476AEA}')\n    PublicLibraries         = UUID('{48DAF80B-E6CF-4F4E-B800-0E69D84EE384}')\n    PublicMusic             = UUID('{3214FAB5-9757-4298-BB61-92A9DEAA44FF}')\n    PublicPictures          = UUID('{B6EBFB86-6907-413C-9AF7-4FC2ABF07CC5}')\n    PublicRingtones         = UUID('{E555AB60-153B-4D17-9F04-A5FE99FC15EC}')\n    PublicUserTiles         = UUID('{0482af6c-08f1-4c34-8c90-e17ec98b1e17}')\n    PublicVideos            = UUID('{2400183A-6185-49FB-A2D8-4A392A602BA3}')\n    QuickLaunch             = UUID('{52a4f021-7b75-48a9-9f6b-4b87a210bc8f}')\n    Recent                  = UUID('{AE50C081-EBD2-438A-8655-8A092E34987A}')\n    RecordedTVLibrary       = UUID('{1A6FDBA2-F42D-4358-A798-B74D745926C5}')\n    ResourceDir             = UUID('{8AD10C31-2ADB-4296-A8F7-E4701232C972}')\n    Ringtones               = UUID('{C870044B-F49E-4126-A9C3-B52A1FF411E8}')\n    RoamingAppData          = UUID('{3EB685DB-65F9-4CF6-A03A-E3EF65729F3D}')\n    RoamedTileImages        = UUID('{AAA8D5A5-F1D6-4259-BAA8-78E7EF60835E}')\n    RoamingTiles            = UUID('{00BCFC5A-ED94-4e48-96A1-3F6217F21990}')\n    SampleMusic             = UUID('{B250C668-F57D-4EE1-A63C-290EE7D1AA1F}')\n    SamplePictures          = UUID('{C4900540-2379-4C75-844B-64E6FAF8716B}')\n    SamplePlaylists         = UUID('{15CA69B3-30EE-49C1-ACE1-6B5EC372AFB5}')\n    SampleVideos            = UUID('{859EAD94-2E85-48AD-A71A-0969CB56A6CD}')\n    SavedGames              = UUID('{4C5C32FF-BB9D-43b0-B5B4-2D72E54EAAA4}')\n    SavedSearches           = UUID('{7d1d3a04-debb-4115-95cf-2f29da2920da}')\n    Screenshots             = UUID('{b7bede81-df94-4682-a7d8-57a52620b86f}')\n    SearchHistory           = UUID('{0D4C3DB6-03A3-462F-A0E6-08924C41B5D4}')\n    SearchTemplates         = UUID('{7E636BFE-DFA9-4D5E-B456-D7B39851D8A9}')\n    SendTo                  = UUID('{8983036C-27C0-404B-8F08-102D10DCFD74}')\n    SidebarDefaultParts     = UUID('{7B396E54-9EC5-4300-BE0A-2482EBAE1A26}')\n    SidebarParts            = UUID('{A75D362E-50FC-4fb7-AC2C-A8BEAA314493}')\n    SkyDrive                = UUID('{A52BBA46-E9E1-435f-B3D9-28DAA648C0F6}')\n    SkyDriveCameraRoll      = UUID('{767E6811-49CB-4273-87C2-20F355E1085B}')\n    SkyDriveDocuments       = UUID('{24D89E24-2F19-4534-9DDE-6A6671FBB8FE}')\n    SkyDrivePictures        = UUID('{339719B5-8C47-4894-94C2-D8F77ADD44A6}')\n    StartMenu               = UUID('{625B53C3-AB48-4EC1-BA1F-A1EF4146FC19}')\n    Startup                 = UUID('{B97D20BB-F46A-4C97-BA10-5E3608430854}')\n    System                  = UUID('{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}')\n    SystemX86               = UUID('{D65231B0-B2F1-4857-A4CE-A8E7C6EA7D27}')\n    Templates               = UUID('{A63293E8-664E-48DB-A079-DF759E0509F7}')\n    UserPinned              = UUID('{9E3995AB-1F9C-4F13-B827-48B24B6C7174}')\n    UserProfiles            = UUID('{0762D272-C50A-4BB0-A382-697DCD729B80}')\n    UserProgramFiles        = UUID('{5CD7AEE2-2219-4A67-B85D-6C9CE15660CB}')\n    UserProgramFilesCommon  = UUID('{BCBD3057-CA5C-4622-B42D-BC56DB0AE516}')\n    Videos                  = UUID('{18989B1D-99B5-455B-841C-AB7C74E4DDFC}')\n    VideosLibrary           = UUID('{491E922F-5643-4AF4-A7EB-4E7A138D8174}')\n    Windows                 = UUID('{F38BF404-1D43-42F2-9305-67DE0B28FC23}')\n\n\n# http://msdn.microsoft.com/en-us/library/windows/desktop/bb762188.aspx\nclass UserHandle:\n    current = wintypes.HANDLE(0)\n    common = wintypes.HANDLE(-1)\n\n\n# http://msdn.microsoft.com/en-us/library/windows/desktop/ms680722.aspx\n_CoTaskMemFree = windll.ole32.CoTaskMemFree\n_CoTaskMemFree.restype = None\n_CoTaskMemFree.argtypes = [ctypes.c_void_p]\n\n\n# http://msdn.microsoft.com/en-us/library/windows/desktop/bb762188.aspx\n# http://www.themacaque.com/?p=954\n_SHGetKnownFolderPath = windll.shell32.SHGetKnownFolderPath\n_SHGetKnownFolderPath.argtypes = [\n    ctypes.POINTER(GUID), wintypes.DWORD, wintypes.HANDLE, ctypes.POINTER(ctypes.c_wchar_p)\n]\n\n\nclass PathNotFoundException(Exception):\n    pass\n\n\ndef get_path(folderid, user_handle=UserHandle.common):\n    fid = GUID(folderid)\n    pPath = ctypes.c_wchar_p()\n    S_OK = 0\n    if _SHGetKnownFolderPath(ctypes.byref(fid), 0, user_handle, ctypes.byref(pPath)) != S_OK:\n        raise PathNotFoundException()\n    path = pPath.value\n    _CoTaskMemFree(pPath)\n    return path\n\n\nif __name__ == '__main__':\n    if len(sys.argv) < 2 or sys.argv[1] in ['-?', '/?']:\n        print('python winpaths.py FOLDERID {current|common}')\n        sys.exit(0)\n\n    try:\n        folderid = getattr(FOLDERID, sys.argv[1])\n    except AttributeError:\n        print('Unknown folder id \"%s\"' % sys.argv[1], file=sys.stderr)\n        sys.exit(1)\n\n    try:\n        if len(sys.argv) == 2:\n            print(get_path(folderid))\n        else:\n            print(get_path(folderid, getattr(UserHandle, sys.argv[2])))\n    except PathNotFoundException:\n        print('Folder not found \"%s\"' % ' '.join(sys.argv[1:]), file=sys.stderr)\n        sys.exit(1)\n"
  },
  {
    "path": "scripts/Dockerfile.lbry_orchstr8",
    "content": "FROM debian:buster-slim\n\nARG TORBA_VERSION=master\n\nRUN apt-get update && \\\n    apt-get upgrade -y && \\\n    apt-get install -y --no-install-recommends \\\n    build-essential \\\n    git \\\n    python3.7 \\\n    python3.7-dev \\\n    python3-pip && \\\n    rm -rf /var/lib/apt/lists/*\n\nRUN python3.7 -m pip install --upgrade pip setuptools wheel\n\nCOPY . /app\nWORKDIR /app\n\nRUN python3.7 -m pip install --user git+https://github.com/lbryio/torba.git@${TORBA_VERSION}#egg=torba\nRUN python3.7 -m pip install -e .\n\n# Orchstr8 API\nEXPOSE 7954\n# Wallet Server\nEXPOSE 5280\n# SPV Server\nEXPOSE 50002\n# blockchain\nEXPOSE 9246\nENV TORBA_LEDGER lbry.wallet\n\nRUN /usr/local/bin/orchstr8 download\nENTRYPOINT [\"/usr/local/bin/orchstr8\"]\n"
  },
  {
    "path": "scripts/check_signature.py",
    "content": "import argparse\nimport sqlite3\nfrom binascii import hexlify\nfrom lbry.wallet.transaction import Output\n\n\ndef check(db_path, claim_id):\n    db = sqlite3.connect(db_path)\n    db.row_factory = sqlite3.Row\n    claim = db.execute('select * from claim where claim_id=?', (claim_id,)).fetchone()\n    if not claim:\n        print('Could not find claim.')\n        return\n    channel = db.execute('select * from claim where claim_hash=?', (claim['channel_hash'],)).fetchone()\n    if not channel:\n        print('Could not find channel for this claim.')\n    print(f\"Claim: {claim['claim_name']}\")\n    print(f\"Channel: {channel['claim_name']}\")\n    print(f\"Signature: {hexlify(claim['signature']).decode()}\")\n    print(f\"Digest: {hexlify(claim['signature_digest']).decode()}\")\n    print(f\"Pubkey: {hexlify(channel['public_key_bytes']).decode()}\")\n    print(\"Valid: {}\".format(Output.is_signature_valid(\n        claim['signature'], claim['signature_digest'], channel['public_key_bytes']\n    )))\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser()\n    parser.add_argument('db_path')\n    parser.add_argument('claim_id')\n    args = parser.parse_args()\n    check(args.db_path, args.claim_id)\n"
  },
  {
    "path": "scripts/check_video.py",
    "content": "#!/usr/bin/env python3\n\nimport asyncio\nimport logging\nimport platform\nimport sys\n\n# noinspection PyUnresolvedReferences\nimport lbry.wallet  # needed to make the following line work (it's a bug):\nfrom lbry.conf import TranscodeConfig\nfrom lbry.file_analysis import VideoFileAnalyzer\n\n\ndef enable_logging():\n    root = logging.getLogger()\n    root.setLevel(logging.DEBUG)\n\n    handler = logging.StreamHandler(sys.stdout)\n    handler.setLevel(logging.DEBUG)\n    formatter = logging.Formatter('%(message)s')\n    handler.setFormatter(formatter)\n    root.addHandler(handler)\n\n\nasync def process_video(analyzer, video_file):\n    try:\n        await analyzer.verify_or_repair(True, False, video_file)\n        print(\"No concerns. Ship it!\")\n    except (FileNotFoundError, ValueError) as e:\n        print(\"Analysis failed.\", str(e))\n    except Exception as e:\n        print(str(e))\n        transcode = input(\"Would you like to make a repaired clone now? [y/N] \")\n        if transcode == \"y\":\n            try:\n                new_video_file, _ = await analyzer.verify_or_repair(True, True, video_file)\n                print(\"Successfully created \", new_video_file)\n            except Exception as e:\n                print(\"Unable to complete the transcode. Message: \", str(e))\n\n\ndef main():\n    if len(sys.argv) < 2:\n        print(\"Usage: check_video.py <path to video file>\", file=sys.stderr)\n        sys.exit(1)\n\n    enable_logging()\n\n    video_file = sys.argv[1]\n    conf = TranscodeConfig()\n    analyzer = VideoFileAnalyzer(conf)\n    try:\n        asyncio.run(process_video(analyzer, video_file))\n    except KeyboardInterrupt:\n        pass\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "scripts/checkpoints.py",
    "content": "import asyncio\nimport os\n\nfrom lbry.extras.cli import ensure_directory_exists\nfrom lbry.conf import Config\nfrom lbry.wallet.header import Headers\nimport lbry.wallet.checkpoints\n\n\nasync def main():\n    outpath = lbry.wallet.checkpoints.__file__\n    ledger_path = os.path.join(Config().wallet_dir, 'lbc_mainnet')\n    ensure_directory_exists(ledger_path)\n    headers_path = os.path.join(ledger_path, 'headers')\n    headers = Headers(headers_path)\n    await headers.open()\n    print(f\"Working on headers at {outpath}\")\n    print(\"Verifying integrity, might take a while.\")\n    await headers.repair()\n    target = ((headers.height - 100) // 1000) * 1000\n    current_checkpoint_tip = max(lbry.wallet.checkpoints.HASHES.keys())\n    if target <= current_checkpoint_tip:\n        print(f\"We have nothing to add: Local: {target}, checkpoint: {current_checkpoint_tip}\")\n        return\n    print(f\"Headers file at {headers.height}, checkpointing up to {target}.\"\n          f\"Current checkpoint at {current_checkpoint_tip}.\")\n    with open(outpath, 'w') as outfile:\n        print('HASHES = {', file=outfile)\n        for height in range(0, target, 1000):\n            print(f\"    {height}: '{headers.chunk_hash(height, 1000)}',\", file=outfile)\n        print('}', file=outfile)\n\n\nif __name__ == \"__main__\":\n    asyncio.get_event_loop().run_until_complete(main())\n"
  },
  {
    "path": "scripts/checktrie.py",
    "content": "import sys\nimport asyncio\nfrom binascii import hexlify\n\nfrom lbry.wallet.server.db.writer import SQLDB\nfrom lbry.wallet.server.coin import LBC\nfrom lbry.wallet.server.daemon import Daemon\n\n\ndef hex_reverted(value: bytes) -> str:\n    return hexlify(value[::-1]).decode()\n\n\ndef match(name, what, value, expected):\n    if value != expected:\n        print(f'{name}: {what} mismatch, {value} is not {expected}')\n    return value == expected\n\n\ndef checkrecord(record, expected_winner, expected_claim):\n    assert record['is_controlling'] == record['claim_hash'], dict(record)\n    name = record['normalized']\n    claim_id = hex_reverted(record['claim_hash'])\n    takover = record['activation_height']\n    if not expected_winner:\n        print(f\"{name} not on lbrycrd. We have {claim_id} at {takover} takeover height.\")\n        return\n    if not match(name, 'claim id', claim_id, expected_winner['claimId']):\n        print(f\"-- {name} has the wrong winner\")\n    if not expected_claim:\n        print(f'{name}: {claim_id} not found, we possibly have an abandoned claim as winner')\n        return\n    match(name, 'height', record['height'], expected_claim['height'])\n    match(name, 'activation height', takover, expected_claim['valid at height'])\n    match(name, 'name', record['normalized'], expected_claim['normalized_name'])\n    match(name, 'amount', record['amount'], expected_claim['amount'])\n    match(name, 'effective amount', record['effective_amount'], expected_claim['effective amount'])\n    match(name, 'txid', hex_reverted(record['txo_hash'][:-4]), expected_claim['txid'])\n    match(name, 'nout', int.from_bytes(record['txo_hash'][-4:], 'little', signed=False), expected_claim['n'])\n\n\nasync def checkcontrolling(daemon: Daemon, db: SQLDB):\n    records, names, futs = [], [], []\n    for record in db.get_claims('claimtrie.claim_hash as is_controlling, claim.*', is_controlling=True):\n        records.append(record)\n        claim_id = hex_reverted(record['claim_hash'])\n        names.append((record['normalized'], (claim_id,), \"\", True))  # last parameter is IncludeValues\n        if len(names) > 50000:\n            futs.append(daemon._send_vector('getclaimsfornamebyid', names))\n            names.clear()\n    if names:\n        futs.append(daemon._send_vector('getclaimsfornamebyid', names))\n        names.clear()\n\n    while futs:\n        winners, claims = futs.pop(0), futs.pop(0)\n        for winner, claim in zip(await winners, await claims):\n            checkrecord(records.pop(0), winner, claim)\n\n\nif __name__ == '__main__':\n    if len(sys.argv) != 3:\n        print(\"usage: <db_file_path> <lbrycrd_url>\")\n        sys.exit(1)\n    db_path, lbrycrd_url = sys.argv[1:]  # pylint: disable=W0632\n    daemon = Daemon(LBC(), url=lbrycrd_url)\n    db = SQLDB(None, db_path)\n    db.open()\n\n    asyncio.get_event_loop().run_until_complete(checkcontrolling(daemon, db))\n"
  },
  {
    "path": "scripts/deploy_dev_wallet_server.sh",
    "content": "#!/usr/bin/env bash\n\n# usage: update_dev_wallet_server.sh <host to update>\nTARGET_HOST=$1\n\nSCRIPTS_DIR=`dirname $0`\nLBRY_DIR=`dirname $SCRIPTS_DIR`\n\n# build the image\ndocker build -f $LBRY_DIR/docker/Dockerfile.wallet_server -t lbry/wallet-server:development $LBRY_DIR\nIMAGE=`docker image inspect lbry/wallet-server:development | sed -n \"s/^.*Id\\\":\\s*\\\"sha256:\\s*\\(\\S*\\)\\\".*$/\\1/p\"`\n\n# push the image to the server\nssh $TARGET_HOST docker image prune --force\ndocker save $IMAGE | ssh $TARGET_HOST docker load\nssh $TARGET_HOST docker tag $IMAGE lbry/wallet-server:development\n\n# restart the wallet server\nssh $TARGET_HOST docker-compose down\nssh $TARGET_HOST WALLET_SERVER_TAG=\"development\" docker-compose up -d\n"
  },
  {
    "path": "scripts/dht_crawler.py",
    "content": "import sys\nimport datetime\nimport logging\nimport asyncio\nimport os.path\nimport random\nimport time\nimport typing\nfrom dataclasses import dataclass, astuple, replace\n\nfrom aiohttp import web\nfrom prometheus_client import Gauge, generate_latest as prom_generate_latest, Counter, Histogram\n\nimport lbry.dht.error\nfrom lbry.dht.constants import generate_id\nfrom lbry.dht.node import Node\nfrom lbry.dht.peer import make_kademlia_peer, PeerManager, decode_tcp_peer_from_compact_address\nfrom lbry.dht.protocol.distance import Distance\nfrom lbry.dht.protocol.iterative_find import FindValueResponse, FindNodeResponse, FindResponse\nfrom lbry.extras.daemon.storage import SQLiteMixin\nfrom lbry.conf import Config\nfrom lbry.utils import resolve_host\n\n\nlogging.basicConfig(level=logging.INFO, format=\"%(asctime)s %(levelname)-4s %(name)s:%(lineno)d: %(message)s\")\nlog = logging.getLogger(__name__)\n\n\nclass SDHashSamples:\n    def __init__(self, samples_file_path):\n        with open(samples_file_path, \"rb\") as sample_file:\n            self._samples = sample_file.read()\n        assert len(self._samples) % 48 == 0\n        self.size = len(self._samples) // 48\n\n    def read_samples(self, count=1):\n        for _ in range(count):\n            offset = 48 * random.randrange(0, self.size)\n            yield self._samples[offset:offset + 48]\n\n\nclass PeerStorage(SQLiteMixin):\n    CREATE_TABLES_QUERY = \"\"\"\n    PRAGMA JOURNAL_MODE=WAL;\n    CREATE TABLE IF NOT EXISTS peer (\n        peer_id INTEGER NOT NULL,\n        node_id VARCHAR(96),\n        address VARCHAR,\n        udp_port INTEGER,\n        tcp_port INTEGER,\n        first_online DATETIME,\n        errors INTEGER,\n        last_churn INTEGER,\n        added_on DATETIME NOT NULL,\n        last_check DATETIME,\n        last_seen DATETIME,\n        latency INTEGER,\n        PRIMARY KEY (peer_id)\n    );\n    CREATE TABLE IF NOT EXISTS connection (\n        from_peer_id INTEGER NOT NULL,\n        to_peer_id INTEGER NOT NULL,\n        PRIMARY KEY (from_peer_id, to_peer_id),\n        FOREIGN KEY(from_peer_id) REFERENCES peer (peer_id),\n        FOREIGN KEY(to_peer_id) REFERENCES peer (peer_id)\n    );\n\"\"\"\n\n    async def open(self):\n        await super().open()\n        self.db.writer_connection.row_factory = dict_row_factory\n\n    async def all_peers(self):\n        return [\n            DHTPeer(**peer) for peer in await self.db.execute_fetchall(\n                \"select * from peer where latency > 0 or last_seen > datetime('now', '-1 hour')\")\n        ]\n\n    async def save_peers(self, *peers):\n        log.info(\"Saving graph nodes (peers) to DB\")\n        await self.db.executemany(\n            \"INSERT OR REPLACE INTO peer(\"\n            \"node_id, address, udp_port, tcp_port, first_online, errors, last_churn,\"\n            \"added_on, last_check, last_seen, latency, peer_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)\",\n            [astuple(peer) for peer in peers]\n        )\n        log.info(\"Finished saving graph nodes (peers) to DB\")\n\n    async def save_connections(self, connections_map):\n        log.info(\"Saving graph edges (connections) to DB\")\n        await self.db.executemany(\n            \"DELETE FROM connection WHERE from_peer_id = ?\", [(key,) for key in connections_map])\n        for from_peer_id in connections_map:\n            await self.db.executemany(\n                \"INSERT INTO connection(from_peer_id, to_peer_id) VALUES(?,?)\",\n                [(from_peer_id, to_peer_id) for to_peer_id in connections_map[from_peer_id]])\n        log.info(\"Finished saving graph edges (connections) to DB\")\n\n\n@dataclass(frozen=True)\nclass DHTPeer:\n    node_id: str\n    address: str\n    udp_port: int\n    tcp_port: int = None\n    first_online: datetime.datetime = None\n    errors: int = None\n    last_churn: int = None\n    added_on: datetime.datetime = None\n    last_check: datetime.datetime = None\n    last_seen: datetime.datetime = None\n    latency: int = None\n    peer_id: int = None\n\n    @classmethod\n    def from_kad_peer(cls, peer, peer_id):\n        node_id = peer.node_id.hex() if peer.node_id else None\n        return DHTPeer(\n            node_id=node_id, address=peer.address, udp_port=peer.udp_port, tcp_port=peer.tcp_port,\n            peer_id=peer_id, added_on=datetime.datetime.utcnow())\n\n    def to_kad_peer(self):\n        node_id = bytes.fromhex(self.node_id) if self.node_id else None\n        return make_kademlia_peer(node_id, self.address, self.udp_port, self.tcp_port)\n\n\ndef new_node(address=\"0.0.0.0\", udp_port=0, node_id=None):\n    node_id = node_id or generate_id()\n    loop = asyncio.get_event_loop()\n    return Node(loop, PeerManager(loop), node_id, udp_port, udp_port, 3333, address)\n\n\nclass Crawler:\n    unique_total_hosts_metric = Gauge(\n        \"unique_total_hosts\", \"Number of unique hosts seen in the last interval\", namespace=\"dht_crawler_node\",\n    )\n    reachable_hosts_metric = Gauge(\n        \"reachable_hosts\", \"Number of hosts that replied in the last interval\", namespace=\"dht_crawler_node\",\n    )\n    total_historic_hosts_metric = Gauge(\n        \"history_total_hosts\", \"Number of hosts seen since first run.\", namespace=\"dht_crawler_node\",\n    )\n    pending_check_hosts_metric = Gauge(\n        \"pending_hosts\", \"Number of hosts on queue to be checked.\", namespace=\"dht_crawler_node\",\n    )\n    hosts_with_errors_metric = Gauge(\n        \"error_hosts\", \"Number of hosts that raised errors during contact.\", namespace=\"dht_crawler_node\",\n    )\n    ROUTING_TABLE_SIZE_HISTOGRAM_BUCKETS = tuple(map(float, range(100))) + (\n        500., 1000., 2000., float('inf')\n    )\n    connections_found_metric = Histogram(\n        \"connections_found\", \"Number of hosts returned by the last successful contact.\", namespace=\"dht_crawler_node\",\n        buckets=ROUTING_TABLE_SIZE_HISTOGRAM_BUCKETS\n    )\n    known_connections_found_metric = Histogram(\n        \"known_connections_found\", \"Number of already known hosts returned by last contact.\",\n        namespace=\"dht_crawler_node\", buckets=ROUTING_TABLE_SIZE_HISTOGRAM_BUCKETS\n    )\n    reachable_connections_found_metric = Histogram(\n        \"reachable_connections_found\", \"Number of reachable known hosts returned by last contact.\",\n        namespace=\"dht_crawler_node\", buckets=ROUTING_TABLE_SIZE_HISTOGRAM_BUCKETS\n    )\n    LATENCY_HISTOGRAM_BUCKETS = (\n        0., 5., 10., 15., 30., 60., 120., 180., 240., 300., 600., 1200., 1800., 4000., 6000., float('inf')\n    )\n    host_latency_metric = Histogram(\n        \"host_latency\", \"Time spent on the last request, in milliseconds.\", namespace=\"dht_crawler_node\",\n        buckets=LATENCY_HISTOGRAM_BUCKETS\n    )\n    probed_streams_metric = Counter(\n        \"probed_streams\", \"Amount of streams probed.\", namespace=\"dht_crawler_node\",\n    )\n    announced_streams_metric = Counter(\n        \"announced_streams\", \"Amount of streams where announcements were found.\", namespace=\"dht_crawler_node\",\n    )\n    working_streams_metric = Counter(\n        \"working_streams\", \"Amount of streams with reachable hosts.\", namespace=\"dht_crawler_node\",\n    )\n\n    def __init__(self, db_path: str, sd_hash_samples: SDHashSamples):\n        self.node = new_node()\n        self.db = PeerStorage(db_path)\n        self.sd_hashes = sd_hash_samples\n        self._memory_peers = {}\n        self._reachable_by_node_id = {}\n        self._connections = {}\n\n    async def open(self):\n        await self.db.open()\n        self._memory_peers = {\n            (peer.address, peer.udp_port): peer for peer in await self.db.all_peers()\n        }\n        self.refresh_reachable_set()\n\n    def refresh_reachable_set(self):\n        self._reachable_by_node_id = {\n            bytes.fromhex(peer.node_id): peer for peer in self._memory_peers.values() if (peer.latency or 0) > 0\n        }\n\n    async def probe_files(self):\n        if not self.sd_hashes:\n            return\n        while True:\n            for sd_hash in self.sd_hashes.read_samples(10_000):\n                self.refresh_reachable_set()\n                distance = Distance(sd_hash)\n                node_ids = list(self._reachable_by_node_id.keys())\n                node_ids.sort(key=lambda node_id: distance(node_id))\n                k_closest = [self._reachable_by_node_id[node_id] for node_id in node_ids[:8]]\n                found = False\n                working = False\n                for response in asyncio.as_completed(\n                        [self.request_peers(peer.address, peer.udp_port, peer.node_id, sd_hash) for peer in k_closest]):\n                    response = await response\n                    if response and response.found:\n                        found = True\n                        blob_peers = []\n                        for compact_addr in response.found_compact_addresses:\n                            try:\n                                blob_peers.append(decode_tcp_peer_from_compact_address(compact_addr))\n                            except ValueError as e:\n                                log.error(\"Error decoding compact peers: %s\", e)\n                        for blob_peer in blob_peers:\n                            response = await self.request_peers(blob_peer.address, blob_peer.tcp_port, blob_peer.node_id, sd_hash)\n                            if response:\n                                working = True\n                                log.info(\"Found responsive peer for %s: %s:%d(%d)\",\n                                         sd_hash.hex()[:8], blob_peer.address,\n                                         blob_peer.udp_port or -1, blob_peer.tcp_port or -1)\n                            else:\n                                log.info(\"Found dead peer for %s: %s:%d(%d)\",\n                                         sd_hash.hex()[:8], blob_peer.address,\n                                         blob_peer.udp_port or -1, blob_peer.tcp_port or -1)\n                self.probed_streams_metric.inc()\n                if found:\n                    self.announced_streams_metric.inc()\n                if working:\n                    self.working_streams_metric.inc()\n                log.info(\"Done querying stream %s for peers. Found: %s, working: %s\", sd_hash.hex()[:8], found, working)\n                await asyncio.sleep(.5)\n\n    @property\n    def refresh_limit(self):\n        return datetime.datetime.utcnow() - datetime.timedelta(hours=1)\n\n    @property\n    def all_peers(self):\n        return [\n            peer for peer in self._memory_peers.values()\n            if (peer.last_seen and peer.last_seen > self.refresh_limit) or (peer.latency or 0) > 0\n        ]\n\n    @property\n    def active_peers_count(self):\n        return len(self.all_peers)\n\n    @property\n    def checked_peers_count(self):\n        return len([peer for peer in self.all_peers if peer.last_check and peer.last_check > self.refresh_limit])\n\n    @property\n    def unreachable_peers_count(self):\n        return len([peer for peer in self.all_peers\n                    if peer.last_check and peer.last_check > self.refresh_limit and not peer.latency])\n\n    @property\n    def peers_with_errors_count(self):\n        return len([peer for peer in self.all_peers if (peer.errors or 0) > 0])\n\n    def get_peers_needing_check(self):\n        to_check = [peer for peer in self.all_peers if peer.last_check is None or peer.last_check < self.refresh_limit]\n        return to_check\n\n    def remove_expired_peers(self):\n        for key, peer in list(self._memory_peers.items()):\n            if (peer.latency or 0) < 1 and peer.last_seen < self.refresh_limit:\n                del self._memory_peers[key]\n\n    def add_peers(self, *peers):\n        for peer in peers:\n            db_peer = self.get_from_peer(peer)\n            if db_peer and db_peer.node_id is None and peer.node_id is not None:\n                db_peer = replace(db_peer, node_id=peer.node_id.hex())\n            elif not db_peer:\n                db_peer = DHTPeer.from_kad_peer(peer, len(self._memory_peers) + 1)\n            db_peer = replace(db_peer, last_seen=datetime.datetime.utcnow())\n            self._memory_peers[(peer.address, peer.udp_port)] = db_peer\n\n    async def flush_to_db(self):\n        await self.db.save_peers(*self._memory_peers.values())\n        connections_to_save = self._connections\n        self._connections = {}\n        # await self.db.save_connections(connections_to_save)  heavy call\n        self.remove_expired_peers()\n\n    def get_from_peer(self, peer):\n        return self._memory_peers.get((peer.address, peer.udp_port), None)\n\n    def set_latency(self, peer, latency=None):\n        if latency:\n            self.host_latency_metric.observe(latency / 1_000_000.0)\n        db_peer = self.get_from_peer(peer)\n        if not db_peer:\n            return\n        db_peer = replace(db_peer, latency=latency)\n        if not db_peer.node_id and peer.node_id:\n            db_peer = replace(db_peer, node_id=peer.node_id.hex())\n        if db_peer.first_online and latency is None:\n            db_peer = replace(db_peer, last_churn=(datetime.datetime.utcnow() - db_peer.first_online).seconds)\n        elif latency is not None and db_peer.first_online is None:\n            db_peer = replace(db_peer, first_online=datetime.datetime.utcnow())\n        db_peer = replace(db_peer, last_check=datetime.datetime.utcnow())\n        self._memory_peers[(db_peer.address, db_peer.udp_port)] = db_peer\n\n    def inc_errors(self, peer):\n        db_peer = self.get_from_peer(peer)\n        self._memory_peers[(peer.address, peer.node_id)] = replace(db_peer, errors=(db_peer.errors or 0) + 1)\n\n    def associate_peers(self, peer, other_peers):\n        self._connections[self.get_from_peer(peer).peer_id] = [\n            self.get_from_peer(other_peer).peer_id for other_peer in other_peers]\n\n    async def request_peers(self, host, port, node_id, key=None) -> typing.Optional[FindResponse]:\n        key = key or node_id\n        peer = make_kademlia_peer(key, await resolve_host(host, port, 'udp'), port)\n        for attempt in range(3):\n            try:\n                req_start = time.perf_counter_ns()\n                if key == node_id:\n                    response = await self.node.protocol.get_rpc_peer(peer).find_node(key)\n                    response = FindNodeResponse(key, response)\n                    latency = time.perf_counter_ns() - req_start\n                    self.set_latency(peer, latency)\n                else:\n                    response = await self.node.protocol.get_rpc_peer(peer).find_value(key)\n                    response = FindValueResponse(key, response)\n                await asyncio.sleep(0.05)\n                return response\n            except asyncio.TimeoutError:\n                if key == node_id:\n                    self.set_latency(peer, None)\n                continue\n            except lbry.dht.error.TransportNotConnected:\n                log.info(\"Transport unavailable, waiting 1s to retry\")\n                await asyncio.sleep(1)\n            except lbry.dht.error.RemoteException as e:\n                log.info('Peer errored: %s:%d attempt #%d - %s',\n                         host, port, (attempt + 1), str(e))\n                if key == node_id:\n                    self.inc_errors(peer)\n                    self.set_latency(peer, None)\n                continue\n\n    async def crawl_routing_table(self, host, port, node_id=None):\n        start = time.time()\n        log.debug(\"querying %s:%d\", host, port)\n        address = await resolve_host(host, port, 'udp')\n        key = node_id or self.node.protocol.peer_manager.get_node_id_for_endpoint(address, port)\n        peer = make_kademlia_peer(key, address, port)\n        self.add_peers(peer)\n        if not key:\n            latency = None\n            for _ in range(3):\n                try:\n                    ping_start = time.perf_counter_ns()\n                    await self.node.protocol.get_rpc_peer(peer).ping()\n                    await asyncio.sleep(0.05)\n                    key = key or self.node.protocol.peer_manager.get_node_id_for_endpoint(address, port)\n                    peer = make_kademlia_peer(key, address, port)\n                    latency = time.perf_counter_ns() - ping_start\n                    break\n                except asyncio.TimeoutError:\n                    pass\n                except lbry.dht.error.RemoteException:\n                    self.inc_errors(peer)\n                    pass\n            self.set_latency(peer, latency if peer.node_id else None)\n            if not latency or not peer.node_id:\n                if latency and not peer.node_id:\n                    log.warning(\"No node id from %s:%d\", host, port)\n                return set()\n        distance = Distance(key)\n        max_distance = int.from_bytes(bytes([0xff] * 48), 'big')\n        peers = set()\n        factor = 2048\n        for i in range(1000):\n            response = await self.request_peers(address, port, key)\n            new_peers = list(response.get_close_kademlia_peers(peer)) if response else None\n            if not new_peers:\n                break\n            new_peers.sort(key=lambda peer: distance(peer.node_id))\n            peers.update(new_peers)\n            far_key = new_peers[-1].node_id\n            if distance(far_key) <= distance(key):\n                current_distance = distance(key)\n                next_jump = current_distance + int(max_distance // factor)  # jump closer\n                factor /= 2\n                if factor > 8 and next_jump < max_distance:\n                    key = int.from_bytes(peer.node_id, 'big') ^ next_jump\n                    if key.bit_length() > 384:\n                        break\n                    key = key.to_bytes(48, 'big')\n                else:\n                    break\n            else:\n                key = far_key\n                factor = 2048\n        if peers:\n            log.info(\"Done querying %s:%d in %.2f seconds: %d peers found over %d requests.\",\n                     host, port, (time.time() - start), len(peers), i)\n        if peers:\n            self.connections_found_metric.observe(len(peers))\n            known_peers = 0\n            reachable_connections = 0\n            for peer in peers:\n                known_peer = self.get_from_peer(peer)\n                known_peers += 1 if known_peer else 0\n                reachable_connections += 1 if known_peer and (known_peer.latency or 0) > 0 else 0\n            self.known_connections_found_metric.observe(known_peers)\n            self.reachable_connections_found_metric.observe(reachable_connections)\n        self.add_peers(*peers)\n        self.associate_peers(peer, peers)\n        return peers\n\n    async def process(self):\n        to_process = {}\n\n        def submit(_peer):\n            f = asyncio.ensure_future(\n                self.crawl_routing_table(_peer.address, _peer.udp_port, bytes.fromhex(_peer.node_id)))\n            to_process[_peer.peer_id] = f\n            f.add_done_callback(lambda _: to_process.pop(_peer.peer_id))\n\n        to_check = self.get_peers_needing_check()\n        last_flush = datetime.datetime.utcnow()\n        while True:\n            for peer in to_check[:200]:\n                if peer.peer_id not in to_process:\n                    submit(peer)\n                    await asyncio.sleep(.05)\n            await asyncio.sleep(0)\n            self.unique_total_hosts_metric.set(self.checked_peers_count)\n            self.reachable_hosts_metric.set(self.checked_peers_count - self.unreachable_peers_count)\n            self.total_historic_hosts_metric.set(len(self._memory_peers))\n            self.pending_check_hosts_metric.set(len(to_check))\n            self.hosts_with_errors_metric.set(self.peers_with_errors_count)\n            log.info(\"%d known, %d contacted recently, %d unreachable, %d error, %d processing, %d on queue\",\n                     self.active_peers_count, self.checked_peers_count, self.unreachable_peers_count,\n                     self.peers_with_errors_count, len(to_process), len(to_check))\n            if to_process:\n                await asyncio.wait(to_process.values(), return_when=asyncio.FIRST_COMPLETED)\n            to_check = self.get_peers_needing_check()\n            if (datetime.datetime.utcnow() - last_flush).seconds > 60:\n                log.info(\"flushing to db\")\n                await self.flush_to_db()\n                last_flush = datetime.datetime.utcnow()\n            while not to_check and not to_process:\n                port = self.node.listening_port.get_extra_info('socket').getsockname()[1]\n                self.node.stop()\n                await self.node.start_listening()\n                log.info(\"Idle, sleeping a minute. Port changed to %d\", port)\n                await asyncio.sleep(60.0)\n                to_check = self.get_peers_needing_check()\n\n\nclass SimpleMetrics:\n    def __init__(self, port):\n        self.prometheus_port = port\n\n    async def handle_metrics_get_request(self, _):\n        try:\n            return web.Response(\n                text=prom_generate_latest().decode(),\n                content_type='text/plain; version=0.0.4'\n            )\n        except Exception:\n            log.exception('could not generate prometheus data')\n            raise\n\n    async def start(self):\n        prom_app = web.Application()\n        prom_app.router.add_get('/metrics', self.handle_metrics_get_request)\n        metrics_runner = web.AppRunner(prom_app)\n        await metrics_runner.setup()\n        prom_site = web.TCPSite(metrics_runner, \"0.0.0.0\", self.prometheus_port)\n        await prom_site.start()\n\n\ndef dict_row_factory(cursor, row):\n    d = {}\n    for idx, col in enumerate(cursor.description):\n        if col[0] in ('added_on', 'first_online', 'last_seen', 'last_check'):\n            d[col[0]] = datetime.datetime.fromisoformat(row[idx]) if row[idx] else None\n        else:\n            d[col[0]] = row[idx]\n    return d\n\n\nasync def test():\n    db_path = \"/tmp/peers.db\" if len(sys.argv) == 1 else sys.argv[-1]\n    asyncio.get_event_loop().set_debug(True)\n    metrics = SimpleMetrics('8080')\n    await metrics.start()\n    conf = Config()\n    hosting_samples = SDHashSamples(\"test.sample\") if os.path.isfile(\"test.sample\") else None\n    crawler = Crawler(db_path, hosting_samples)\n    await crawler.open()\n    await crawler.flush_to_db()\n    await crawler.node.start_listening()\n    if crawler.active_peers_count < 100:\n        probes = []\n        for (host, port) in conf.known_dht_nodes:\n            probes.append(asyncio.create_task(crawler.crawl_routing_table(host, port)))\n        await asyncio.gather(*probes)\n        await crawler.flush_to_db()\n    await asyncio.gather(crawler.process(), crawler.probe_files())\n\nif __name__ == '__main__':\n    asyncio.run(test())\n"
  },
  {
    "path": "scripts/dht_monitor.py",
    "content": "import curses\nimport time\nimport asyncio\nimport lbry.wallet\nfrom lbry.conf import Config\nfrom lbry.extras.daemon.client import daemon_rpc\n\nstdscr = curses.initscr()\n\n\ndef init_curses():\n    curses.noecho()\n    curses.cbreak()\n    stdscr.nodelay(1)\n    stdscr.keypad(1)\n\n\ndef teardown_curses():\n    curses.nocbreak()\n    stdscr.keypad(0)\n    curses.echo()\n    curses.endwin()\n\n\ndef refresh(routing_table_info):\n    height, width = stdscr.getmaxyx()\n\n    node_id = routing_table_info['node_id']\n\n    for y in range(height):\n        stdscr.addstr(y, 0, \" \" * (width - 1))\n\n    buckets = routing_table_info['buckets']\n    stdscr.addstr(0, 0, f\"node id: {node_id}\")\n    stdscr.addstr(1, 0, f\"{len(buckets)} buckets\")\n\n    y = 3\n    for i in range(len(buckets)):\n        stdscr.addstr(y, 0, \"bucket %s\" % i)\n        y += 1\n        for peer in buckets[str(i)]:\n            stdscr.addstr(y, 0, f\"{peer['node_id'][:8]} ({peer['address']}:{peer['udp_port']})\")\n            y += 1\n        y += 1\n\n    stdscr.addstr(y + 1, 0, str(time.time()))\n    stdscr.refresh()\n\n\nasync def main():\n    conf = Config()\n    try:\n        init_curses()\n        c = None\n        while c not in [ord('q'), ord('Q')]:\n            routing_info = await daemon_rpc(conf, 'routing_table_get')\n            refresh(routing_info)\n            c = stdscr.getch()\n            time.sleep(0.1)\n    finally:\n        teardown_curses()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "scripts/dht_node.py",
    "content": "import asyncio\nimport argparse\nimport logging\nimport csv\nimport os.path\nfrom io import StringIO\nfrom typing import Optional\nfrom aiohttp import web\nfrom prometheus_client import generate_latest as prom_generate_latest\n\nfrom lbry.dht.constants import generate_id\nfrom lbry.dht.node import Node\nfrom lbry.dht.peer import PeerManager\nfrom lbry.extras.daemon.storage import SQLiteStorage\nfrom lbry.conf import Config\n\nlogging.basicConfig(level=logging.INFO, format=\"%(asctime)s %(levelname)-4s %(name)s:%(lineno)d: %(message)s\")\nlog = logging.getLogger(__name__)\n\n\nclass SimpleMetrics:\n    def __init__(self, port, node):\n        self.prometheus_port = port\n        self.dht_node: Node = node\n\n    async def handle_metrics_get_request(self, _):\n        try:\n            return web.Response(\n                text=prom_generate_latest().decode(),\n                content_type='text/plain; version=0.0.4'\n            )\n        except Exception:\n            log.exception('could not generate prometheus data')\n            raise\n\n    async def handle_peers_csv(self, _):\n        out = StringIO()\n        writer = csv.DictWriter(out, fieldnames=[\"ip\", \"port\", \"dht_id\"])\n        writer.writeheader()\n        for peer in self.dht_node.protocol.routing_table.get_peers():\n            writer.writerow({\"ip\": peer.address, \"port\": peer.udp_port, \"dht_id\": peer.node_id.hex()})\n        return web.Response(text=out.getvalue(), content_type='text/csv')\n\n    async def handle_blobs_csv(self, _):\n        out = StringIO()\n        writer = csv.DictWriter(out, fieldnames=[\"blob_hash\"])\n        writer.writeheader()\n        for blob in self.dht_node.protocol.data_store.keys():\n            writer.writerow({\"blob_hash\": blob.hex()})\n        return web.Response(text=out.getvalue(), content_type='text/csv')\n\n    async def start(self):\n        prom_app = web.Application()\n        prom_app.router.add_get('/metrics', self.handle_metrics_get_request)\n        if self.dht_node:\n            prom_app.router.add_get('/peers.csv', self.handle_peers_csv)\n            prom_app.router.add_get('/blobs.csv', self.handle_blobs_csv)\n        metrics_runner = web.AppRunner(prom_app)\n        await metrics_runner.setup()\n        prom_site = web.TCPSite(metrics_runner, \"0.0.0.0\", self.prometheus_port)\n        await prom_site.start()\n\n\nasync def main(host: str, port: int, db_file_path: str, bootstrap_node: Optional[str], prometheus_port: int, export: bool):\n    loop = asyncio.get_event_loop()\n    conf = Config()\n    if not db_file_path.startswith(':memory:'):\n        node_id_file_path = db_file_path + 'node_id'\n        if os.path.exists(node_id_file_path):\n            with open(node_id_file_path, 'rb') as node_id_file:\n                node_id = node_id_file.read()\n        else:\n            with open(node_id_file_path, 'wb') as node_id_file:\n                node_id = generate_id()\n                node_id_file.write(node_id)\n\n    storage = SQLiteStorage(conf, db_file_path, loop, loop.time)\n    if bootstrap_node:\n        nodes = bootstrap_node.split(':')\n        nodes = [(nodes[0], int(nodes[1]))]\n    else:\n        nodes = conf.known_dht_nodes\n    await storage.open()\n    node = Node(\n        loop, PeerManager(loop), node_id, port, port, 3333, None,\n        storage=storage, is_bootstrap_node=True\n    )\n    if prometheus_port > 0:\n        metrics = SimpleMetrics(prometheus_port, node if export else None)\n        await metrics.start()\n    node.start(host, nodes)\n    log.info(\"Peer with id %s started\", node_id.hex())\n    while True:\n        await asyncio.sleep(10)\n        log.info(\"Known peers: %d. Storing contact information for %d blobs from %d peers.\",\n                 len(node.protocol.routing_table.get_peers()), len(node.protocol.data_store),\n                 len(node.protocol.data_store.get_storing_contacts()))\n\n\nif __name__ == '__main__':\n    parser = argparse.ArgumentParser(\n        description=\"Starts a single DHT node, which then can be used as a seed node or just a contributing node.\")\n    parser.add_argument(\"--host\", default='0.0.0.0', type=str, help=\"Host to listen for requests. Default: 0.0.0.0\")\n    parser.add_argument(\"--port\", default=4444, type=int, help=\"Port to listen for requests. Default: 4444\")\n    parser.add_argument(\"--db_file\", default='/tmp/dht.db', type=str, help=\"DB file to save peers. Default: /tmp/dht.db\")\n    parser.add_argument(\"--bootstrap_node\", default=None, type=str,\n                        help=\"Node to connect for bootstraping this node. Leave unset to use the default ones. \"\n                             \"Format: host:port Example: lbrynet1.lbry.com:4444\")\n    parser.add_argument(\"--metrics_port\", default=0, type=int, help=\"Port for Prometheus metrics. 0 to disable. Default: 0\")\n    parser.add_argument(\"--enable_csv_export\", action='store_true', help=\"Enable CSV endpoints on metrics server.\")\n    args = parser.parse_args()\n    asyncio.run(main(args.host, args.port, args.db_file, args.bootstrap_node, args.metrics_port, args.enable_csv_export))\n"
  },
  {
    "path": "scripts/download_blob_from_peer.py",
    "content": "\"\"\"A simple script that attempts to directly download a single blob.\n\nTo Do:\n------\nCurrently `lbrynet blob get <hash>` does not work to download single blobs\nwhich are not already present in the system. The function locks up and\nnever returns.\nIt only works for blobs that are in the `blobfiles` directory already.\n\nThis bug is reported in lbryio/lbry-sdk, issue #2070.\n\nMaybe this script can be investigated, and certain parts can be added to\n`lbry.extras.daemon.daemon.jsonrpc_blob_get`\nin order to solve the previous issue, and finally download single blobs\nfrom the network (peers or reflector servers).\n\"\"\"\nimport sys\nimport os\nimport asyncio\nimport socket\nimport ipaddress\nimport lbry.wallet\nfrom lbry.conf import Config\nfrom lbry.extras.daemon.storage import SQLiteStorage\nfrom lbry.blob.blob_manager import BlobManager\nfrom lbry.blob_exchange.client import BlobExchangeClientProtocol, request_blob\nimport logging\n\nlog = logging.getLogger(\"lbry\")\nlog.addHandler(logging.StreamHandler())\nlog.setLevel(logging.DEBUG)\n\n\nasync def main(blob_hash: str, url: str):\n    conf = Config()\n    loop = asyncio.get_running_loop()\n    host_url, port = url.split(\":\")\n    try:\n        host = None\n        if ipaddress.ip_address(host_url):\n            host = host_url\n    except ValueError:\n        host = None\n    if not host:\n        host_info = await loop.getaddrinfo(\n            host_url, 'https',\n            proto=socket.IPPROTO_TCP,\n        )\n        host = host_info[0][4][0]\n\n    storage = SQLiteStorage(conf, os.path.join(conf.data_dir, \"lbrynet.sqlite\"))\n    blob_manager = BlobManager(loop, os.path.join(conf.data_dir, \"blobfiles\"), storage, conf)\n    await storage.open()\n    await blob_manager.setup()\n\n    blob = blob_manager.get_blob(blob_hash)\n    success, keep = await request_blob(loop, blob, host, int(port), conf.peer_connect_timeout,\n                                       conf.blob_download_timeout)\n    print(f\"{'downloaded' if success else 'failed to download'} {blob_hash} from {host}:{port}\\n\"\n          f\"keep connection: {keep}\")\n    if blob.get_is_verified():\n        await blob_manager.delete_blobs([blob.blob_hash])\n        print(f\"deleted {blob_hash}\")\n\n\nif __name__ == \"__main__\":\n    if len(sys.argv) < 2:\n        print(\"usage: download_blob_from_peer.py <blob_hash> [host_url:port]\")\n        sys.exit(1)\n\n    url = 'reflector.lbry.com:5567'\n    if len(sys.argv) > 2:\n        url = sys.argv[2]\n    asyncio.run(main(sys.argv[1], url))\n"
  },
  {
    "path": "scripts/find_max_server_load.py",
    "content": "import time\nimport asyncio\nimport random\nfrom argparse import ArgumentParser\nfrom lbry.wallet.network import ClientSession\n\n\nclass AgentSmith(ClientSession):\n\n    async def do_nefarious_things(self):\n        await self.send_request('blockchain.claimtrie.search', {\n            'no_totals': True,\n            'offset': random.choice(range(0, 300, 20)),\n            'limit': 20,\n            'any_tags': (\n                random.choice([[\n                        random.choice(['gaming', 'games', 'game']) +\n                        random.choice(['entertainment', 'playthrough', 'funny']) +\n                        random.choice(['xbox', 'xbox one', 'xbox news'])\n                    ], [\n                        random.choice(['aliens', 'alien', 'ufo', 'ufos']) +\n                        random.choice(['news', 'sighting', 'sightings'])\n                    ], [\n                        random.choice(['art', 'automotive']),\n                        random.choice(['blockchain', 'economics', 'food']),\n                        random.choice(['funny', 'learnings', 'nature']),\n                        random.choice(['news', 'science', 'technology'])\n                    ]\n                ])\n            ),\n            'not_tags': random.choice([[], [\n                'porn', 'mature', 'xxx', 'nsfw'\n            ]]),\n            'order_by': random.choice([\n                ['release_time'],\n                ['trending_global', 'trending_mixed'],\n                ['effective_amount']\n            ])\n        })\n\n\nclass AgentSmithProgram:\n\n    def __init__(self, host, port):\n        self.host, self.port = host, port\n        self.agent_smiths = []\n\n    async def make_one_more_of_them(self):\n        smith = AgentSmith(network=None, server=(self.host, self.port))\n        await smith.create_connection()\n        self.agent_smiths.append(smith)\n\n    async def coordinate_nefarious_activity(self):\n        start = time.perf_counter()\n        await asyncio.gather(\n            *(s.do_nefarious_things() for s in self.agent_smiths),\n            return_exceptions=True\n        )\n        return time.perf_counter() - start\n\n    def __len__(self):\n        return len(self.agent_smiths)\n\n    async def delete_one_smith(self):\n        if self.agent_smiths:\n            await self.agent_smiths.pop().close()\n\n    async def delete_program(self):\n        await asyncio.gather(*(\n            s.close() for s in self.agent_smiths\n        ))\n\n\nasync def main(host, port):\n    smiths = AgentSmithProgram(host, port)\n    await smiths.make_one_more_of_them()\n    activity = asyncio.create_task(smiths.coordinate_nefarious_activity())\n    ease_off = 0\n    for i in range(1000):\n        await asyncio.sleep(1)\n        if activity.done() and activity.result() < .9:\n            print('more, more, more...')\n            await asyncio.gather(*(\n                asyncio.create_task(smiths.make_one_more_of_them()) for _ in range(20)\n            ))\n        else:\n            print('!!!!!!!!!!!!!!')\n            print('IS NEO LOSING?')\n            print('!!!!!!!!!!!!!!')\n            await asyncio.gather(*(\n                asyncio.create_task(smiths.delete_one_smith()) for _ in range(21)\n            ))\n        print(f'coordinate all {len(smiths)} smiths to action')\n        activity = asyncio.create_task(smiths.coordinate_nefarious_activity())\n    print('finishing up any remaining actions')\n    await activity\n    print('neo has won, deleting agents...')\n    await smiths.delete_program()\n    print('done.')\n\n\nif __name__ == \"__main__\":\n    parser = ArgumentParser()\n    parser.add_argument('--host', dest='host', default='localhost', type=str)\n    parser.add_argument('--port', dest='port', default=50001, type=int)\n    args = parser.parse_args()\n    asyncio.run(main(args.host, args.port))\n"
  },
  {
    "path": "scripts/generate_json_api.py",
    "content": "import os\nimport re\nimport json\nimport inspect\nimport tempfile\nimport asyncio\nimport time\nfrom docopt import docopt\nfrom binascii import unhexlify\nfrom textwrap import indent\nfrom lbry.testcase import CommandTestCase\nfrom lbry.extras.cli import set_kwargs, get_argument_parser\nfrom lbry.extras.daemon.daemon import (\n    Daemon, jsonrpc_dumps_pretty, encode_pagination_doc\n)\nfrom lbry.extras.daemon.json_response_encoder import (\n    encode_tx_doc, encode_txo_doc, encode_account_doc, encode_file_doc,\n    encode_wallet_doc\n)\n\n\nRETURN_DOCS = {\n    'Account': encode_account_doc(),\n    'Wallet': encode_wallet_doc(),\n    'File': encode_file_doc(),\n    'Transaction': encode_tx_doc(),\n    'Output': encode_txo_doc(),\n    'Address': 'an address in base58',\n    'Dict': 'glorious data in dictionary',\n}\n\n\nclass ExampleRecorder:\n    def __init__(self, test):\n        self.test = test\n        self.examples = {}\n\n    async def __call__(self, title, *command):\n        parser = get_argument_parser()\n        args, command_args = parser.parse_known_args(command)\n\n        api_method_name = args.api_method_name\n        parsed = docopt(args.doc, command_args)\n        kwargs = set_kwargs(parsed)\n        for k, v in kwargs.items():\n            if v and isinstance(v, str) and (v[0], v[-1]) == ('\"', '\"'):\n                kwargs[k] = v[1:-1]\n        params = json.dumps({\"method\": api_method_name, \"params\": kwargs})\n\n        method = getattr(self.test.daemon, f'jsonrpc_{api_method_name}')\n        result = method(**kwargs)\n        if asyncio.iscoroutine(result):\n            result = await result\n        output = jsonrpc_dumps_pretty(result, ledger=self.test.daemon.ledger)\n        self.examples.setdefault(api_method_name, []).append({\n            'title': title,\n            'curl': f\"curl -d'{params}' http://localhost:5279/\",\n            'lbrynet': 'lbrynet ' + ' '.join(command),\n            'python': f'requests.post(\"http://localhost:5279\", json={params}).json()',\n            'output': output.strip()\n        })\n        return json.loads(output)['result']\n\n\nclass Examples(CommandTestCase):\n\n    async def asyncSetUp(self):\n        await super().asyncSetUp()\n        self.recorder = ExampleRecorder(self)\n\n    async def play(self):\n        r = self.recorder\n\n        # general sdk\n\n        await r(\n            'Get status',\n            'status'\n        )\n        await r(\n            'Get version',\n            'version'\n        )\n\n        # settings\n\n        await r(\n            'Get settings',\n            'settings', 'get'\n        )\n\n        await r(\n            'Set settings',\n            'settings', 'set', '\"tcp_port\"', '99'\n        )\n\n        # preferences\n\n        await r(\n            'Set preference',\n            'preference', 'set', '\"theme\"', '\"dark\"'\n        )\n\n        await r(\n            'Get preferences',\n            'preference', 'get'\n        )\n\n        # wallets\n\n        await r(\n            'List your wallets',\n            'wallet', 'list'\n        )\n\n        # accounts\n\n        await r(\n            'List your accounts',\n            'account', 'list'\n        )\n\n        account = await r(\n            'Create an account',\n            'account', 'create', '\"generated account\"'\n        )\n\n        await r(\n            'Remove an account',\n            'account', 'remove', account['id']\n        )\n\n        await r(\n            'Add an account from seed',\n            'account', 'add', '\"new account\"', f\"--seed=\\\"{account['seed']}\\\"\"\n        )\n\n        await r(\n            'Modify maximum number of times a change address can be reused',\n            'account', 'set', account['id'], '--change_max_uses=10'\n        )\n\n        # addresses\n\n        await r(\n            'List addresses in default account',\n            'address', 'list'\n        )\n\n        an_address = await r(\n            'Get an unused address',\n            'address', 'unused'\n        )\n\n        address_list_by_id = await r(\n            'List addresses in specified account',\n            'address', 'list', f\"--account_id=\\\"{account['id']}\\\"\"\n        )\n\n        await r(\n            'Check if address is mine',\n            'address', 'is_mine', an_address\n        )\n\n        # sends/funds\n\n        transfer = await r(\n            'Transfer 2 LBC from default account to specific account',\n            'account', 'fund', f\"--to_account=\\\"{account['id']}\\\"\", \"--amount=2.0\", \"--broadcast\"\n        )\n\n        await self.on_transaction_dict(transfer)\n        await self.generate(1)\n        await self.on_transaction_dict(transfer)\n\n        await r(\n            'Get default account balance',\n            'account', 'balance'\n        )\n\n        txlist = await r(\n            'List your transactions',\n            'transaction', 'list'\n        )\n\n        await r(\n            'Get balance for specific account by id',\n            'account', 'balance', f\"\\\"{account['id']}\\\"\"\n        )\n\n        spread_transaction = await r(\n            'Spread LBC between multiple addresses',\n            'account', 'fund', f\"--to_account=\\\"{account['id']}\\\"\", f\"--from_account=\\\"{account['id']}\\\"\", '--amount=1.5', '--outputs=2', '--broadcast'\n        )\n\n        await self.on_transaction_dict(spread_transaction)\n        await self.generate(1)\n        await self.on_transaction_dict(spread_transaction)\n\n        await r(\n            'Transfer all LBC to a specified account',\n            'account', 'fund', f\"--from_account=\\\"{account['id']}\\\"\", \"--everything\", \"--broadcast\"\n        )\n\n        # channels\n\n        channel = await r(\n            'Create a channel claim without metadata',\n            'channel', 'create', '@channel', '1.0'\n        )\n        channel_id = self.get_claim_id(channel)\n        await self.on_transaction_dict(channel)\n        await self.generate(1)\n        await self.on_transaction_dict(channel)\n\n        await r(\n            'List your channel claims',\n            'channel', 'list'\n        )\n\n        await r(\n            'Paginate your channel claims',\n            'channel', 'list', '--page=1', '--page_size=20'\n        )\n\n        channel = await r(\n            'Update a channel claim',\n            'channel', 'update', self.get_claim_id(channel), '--title=\"New Channel\"'\n        )\n\n        await self.on_transaction_dict(channel)\n        await self.generate(1)\n        await self.on_transaction_dict(channel)\n\n        big_channel = await r(\n            'Create a channel claim with all metadata',\n            'channel', 'create', '@bigchannel', '1.0',\n            '--title=\"Big Channel\"', '--description=\"A channel with lots of videos.\"',\n            '--email=\"creator@smallmedia.com\"', '--tags=music', '--tags=art',\n            '--languages=pt-BR', '--languages=uk', '--locations=BR', '--locations=UA::Kiyv',\n            '--website_url=\"http://smallmedia.com\"', '--thumbnail_url=\"http://smallmedia.com/logo.jpg\"',\n            '--cover_url=\"http://smallmedia.com/logo.jpg\"'\n        )\n        await self.on_transaction_dict(big_channel)\n        await self.generate(1)\n        await self.on_transaction_dict(big_channel)\n        await self.daemon.jsonrpc_channel_abandon(self.get_claim_id(big_channel))\n        await self.generate(1)\n\n        # stream claims\n\n        with tempfile.NamedTemporaryFile() as file:\n            file.write(b'hello world')\n            file.flush()\n            stream = await r(\n                'Create a stream claim without metadata',\n                'stream', 'create', 'astream', '1.0', file.name\n            )\n            await self.on_transaction_dict(stream)\n            await self.generate(1)\n            await self.on_transaction_dict(stream)\n        stream_id = self.get_claim_id(stream)\n        stream_name = stream['outputs'][0]['name']\n        stream = await r(\n            'Update a stream claim to add channel',\n            'stream', 'update', stream_id,\n            f'--channel_id=\"{channel_id}\"'\n        )\n        await self.on_transaction_dict(stream)\n        await self.generate(1)\n        await self.on_transaction_dict(stream)\n\n        await r(\n            'List all your claims',\n            'claim', 'list'\n        )\n\n        await r(\n            'Paginate your claims',\n            'claim', 'list', '--page=1', '--page_size=20'\n        )\n\n        await r(\n            'List all your stream claims',\n            'stream', 'list'\n        )\n\n        await r(\n            'Paginate your stream claims',\n            'stream', 'list', '--page=1', '--page_size=20'\n        )\n\n        await r(\n            'Search for all claims in channel',\n            'claim', 'search', '--channel=@channel'\n        )\n\n        await r(\n            'Search for claims matching a name',\n            'claim', 'search', f'--name=\"{stream_name}\"'\n        )\n\n        with tempfile.NamedTemporaryFile(suffix='.png') as file:\n            file.write(unhexlify(\n                b'89504e470d0a1a0a0000000d49484452000000050000000708020000004fc'\n                b'510b9000000097048597300000b1300000b1301009a9c1800000015494441'\n                b'5408d763fcffff3f031260624005d4e603004c45030b5286e9ea000000004'\n                b'9454e44ae426082'\n            ))\n            file.flush()\n            big_stream = await r(\n                'Create an image stream claim with all metadata and fee',\n                'stream', 'create', 'blank-image', '1.0', file.name,\n                '--tags=blank', '--tags=art', '--languages=en', '--locations=US:NH:Manchester',\n                '--fee_currency=LBC', '--fee_amount=0.3',\n                '--title=\"Blank Image\"', '--description=\"A blank PNG that is 5x7.\"', '--author=Picaso',\n                '--license=\"Public Domain\"', '--license_url=http://public-domain.org',\n                '--thumbnail_url=\"http://smallmedia.com/thumbnail.jpg\"', f'--release_time={int(time.time())}',\n                f'--channel_id=\"{channel_id}\"'\n            )\n            await self.on_transaction_dict(big_stream)\n            await self.generate(1)\n            await self.on_transaction_dict(big_stream)\n\n            await self.daemon.jsonrpc_channel_abandon(self.get_claim_id(big_stream))\n            await self.generate(1)\n\n        # collections\n        collection = await r(\n            'Create a collection of one stream',\n            'collection', 'create',\n            '--name=tom', '--bid=1.0',\n            f'--channel_id={channel_id}',\n            f'--claims={stream_id}'\n        )\n\n        await self.on_transaction_dict(collection)\n        await self.generate(1)\n        await self.on_transaction_dict(collection)\n\n        await r(\n            'List collections',\n            'collection', 'list',\n            '--resolve', '--resolve_claims=1',\n        )\n\n\n        # files\n\n        file_list_result = (await r(\n            'List local files',\n            'file', 'list'\n        ))['items']\n        file_uri = f\"{file_list_result[0]['claim_name']}#{file_list_result[0]['claim_id']}\"\n        await r(\n            'Resolve a claim',\n            'resolve', file_uri\n        )\n\n        await r(\n            'List files matching a parameter',\n            'file', 'list', f\"--claim_id=\\\"{file_list_result[0]['claim_id']}\\\"\"\n        )\n\n        await r(\n            'Delete a file',\n            'file', 'delete', f\"--claim_id=\\\"{file_list_result[0]['claim_id']}\\\"\"\n        )\n\n        await r(\n            'Get a file',\n            'get', file_uri\n        )\n\n        await r(\n            'Save a file to the downloads directory',\n            'file', 'save', f\"--sd_hash=\\\"{file_list_result[0]['sd_hash']}\\\"\"\n        )\n\n        # blobs\n\n        bloblist = await r(\n            'List your local blobs',\n            'blob', 'list'\n        )\n\n        await r(\n            'Delete a blob',\n            'blob', 'delete', f\"{bloblist['items'][0]}\"\n        )\n\n        # abandon all the things\n\n        abandon_stream = await r(\n            'Abandon a stream claim',\n            'stream', 'abandon', stream_id\n        )\n        await self.on_transaction_dict(abandon_stream)\n        await self.generate(1)\n        await self.on_transaction_dict(abandon_stream)\n\n        abandon_channel = await r(\n            'Abandon a channel claim',\n            'channel', 'abandon', channel_id\n        )\n        await self.on_transaction_dict(abandon_channel)\n        await self.generate(1)\n        await self.on_transaction_dict(abandon_channel)\n\n        with tempfile.NamedTemporaryFile() as file:\n            file.write(b'hello world')\n            file.flush()\n            stream = await r(\n                'Publish a file',\n                'publish', 'a-new-stream', '--bid=1.0', f'--file_path={file.name}'\n            )\n            await self.on_transaction_dict(stream)\n            await self.generate(1)\n            await self.on_transaction_dict(stream)\n\n\ndef get_examples():\n    player = Examples('play')\n    result = player.run()\n    if result.errors:\n        for error in result.errors:\n            print(error[1])\n        raise Exception('See above for errors while running the examples.')\n    return player.recorder.examples\n\n\nSECTIONS = re.compile(\"(.*?)Usage:(.*?)Options:(.*?)Returns:(.*)\", re.DOTALL)\nREQUIRED_OPTIONS = re.compile(r\"\\(<(.*?)>.*?\\)\")\nARGUMENT_NAME = re.compile(\"--([^=]+)\")\nARGUMENT_TYPE = re.compile(r\"\\s*\\((.*?)\\)(.*)\")\n\n\ndef get_return_def(returns):\n    result = returns.strip()\n    if (result[0], result[-1]) == ('{', '}'):\n        obj_type = result[1:-1]\n        if '[' in obj_type:\n            sub_type = obj_type[obj_type.index('[')+1:-1]\n            obj_type = obj_type[:obj_type.index('[')]\n            if obj_type == 'Paginated':\n                obj_def = encode_pagination_doc(RETURN_DOCS[sub_type])\n            elif obj_type == 'List':\n                obj_def = [RETURN_DOCS[sub_type]]\n            else:\n                raise NameError(f'Unknown return type: {obj_type}')\n        else:\n            obj_def = RETURN_DOCS[obj_type]\n        return indent(json.dumps(obj_def, indent=4), ' '*12)\n    return result\n\n\ndef get_api(name, examples):\n    obj = Daemon.callable_methods[name]\n    docstr = inspect.getdoc(obj).strip()\n\n    try:\n        description, usage, options, returns = SECTIONS.search(docstr).groups()\n    except:\n        raise ValueError(f\"Doc string format error for {obj.__name__}.\")\n\n    required = re.findall(REQUIRED_OPTIONS, usage)\n\n    arguments = []\n    for line in options.splitlines():\n        line = line.strip()\n        if not line:\n            continue\n        if line.startswith('--'):\n            arg, desc = line.split(':', 1)\n            arg_name = ARGUMENT_NAME.search(arg).group(1)\n            arg_type, arg_desc = ARGUMENT_TYPE.search(desc).groups()\n            arguments.append({\n                'name': arg_name.strip(),\n                'type': arg_type.strip(),\n                'description': [arg_desc.strip()],\n                'is_required': arg_name in required\n            })\n        elif line == 'None':\n            continue\n        else:\n            arguments[-1]['description'].append(line.strip())\n\n    for arg in arguments:\n        arg['description'] = ' '.join(arg['description'])\n\n    return {\n        'name': name,\n        'description': description.strip(),\n        'arguments': arguments,\n        'returns': get_return_def(returns),\n        'examples': examples\n    }\n\n\ndef write_api(f):\n    examples = get_examples()\n    api_definitions = Daemon.get_api_definitions()\n    apis = {\n        'main': {\n            'doc': 'Ungrouped commands.',\n            'commands': []\n        }\n    }\n    for group_name, group_doc in api_definitions['groups'].items():\n        apis[group_name] = {\n            'doc': group_doc,\n            'commands': []\n        }\n    for method_name, command in api_definitions['commands'].items():\n        if 'replaced_by' in command:\n            continue\n        apis[command['group'] or 'main']['commands'].append(get_api(\n            method_name,\n            examples.get(method_name, [])\n        ))\n    json.dump(apis, f, indent=4)\n\n\nif __name__ == '__main__':\n    parent = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n    html_file = os.path.join(parent, 'docs', 'api.json')\n    with open(html_file, 'w+') as f:\n        write_api(f)\n"
  },
  {
    "path": "scripts/hook-coincurve.py",
    "content": "\"\"\"\nHook for coincurve.\n\"\"\"\n\nimport os.path\nfrom PyInstaller.utils.hooks import get_module_file_attribute\n\ncoincurve_dir = os.path.dirname(get_module_file_attribute('coincurve'))\nbinaries = [(os.path.join(coincurve_dir, 'libsecp256k1.dll'), 'coincurve')]\n"
  },
  {
    "path": "scripts/hook-libtorrent.py",
    "content": "\"\"\"\nHook for libtorrent.\n\"\"\"\n\nimport os\nimport glob\nimport os.path\nfrom PyInstaller.utils.hooks import get_module_file_attribute\nfrom PyInstaller import compat\n\n\ndef get_binaries():\n    if compat.is_win:\n        files = ('c:/Windows/System32/libssl-1_1-x64.dll', 'c:/Windows/System32/libcrypto-1_1-x64.dll')\n        for file in files:\n            if not os.path.isfile(file):\n                print(f\"MISSING {file}\")\n        return [(file, '.') for file in files]\n    return []\n\n\nbinaries = get_binaries()\nfor file in glob.glob(os.path.join(get_module_file_attribute('libtorrent'), 'libtorrent*pyd*')):\n    binaries.append((file, 'libtorrent'))\n"
  },
  {
    "path": "scripts/idea/lbry-sdk.iml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"PYTHON_MODULE\" version=\"4\">\n  <component name=\"NewModuleRootManager\">\n    <content url=\"file://$MODULE_DIR$\">\n      <sourceFolder url=\"file://$MODULE_DIR$/\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/tests\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/scripts\" isTestSource=\"false\" />\n    </content>\n    <orderEntry type=\"jdk\" jdkName=\"Python 3.7 (lbry)\" jdkType=\"Python SDK\" />\n    <orderEntry type=\"sourceFolder\" forTests=\"false\" />\n  </component>\n  <component name=\"TestRunnerService\">\n    <option name=\"PROJECT_TEST_RUNNER\" value=\"Unittests\" />\n  </component>\n</module>\n"
  },
  {
    "path": "scripts/idea/modules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"ProjectModuleManager\">\n    <modules>\n      <module fileurl=\"file://$PROJECT_DIR$/.idea/lbry-sdk.iml\" filepath=\"$PROJECT_DIR$/.idea/lbry-sdk.iml\" />\n    </modules>\n  </component>\n</project>\n"
  },
  {
    "path": "scripts/idea/vcs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"VcsDirectoryMappings\">\n    <mapping directory=\"$PROJECT_DIR$\" vcs=\"Git\" />\n  </component>\n</project>"
  },
  {
    "path": "scripts/initialize_hub_from_snapshot.sh",
    "content": "#!/bin/bash\n\nSNAPSHOT_HEIGHT=\"1072108\"\n\nHUB_VOLUME_PATH=\"/var/lib/docker/volumes/${USER}_wallet_server\"\nES_VOLUME_PATH=\"/var/lib/docker/volumes/${USER}_es01\"\n\nSNAPSHOT_TAR_NAME=\"wallet_server_snapshot_${SNAPSHOT_HEIGHT}.tar.gz\"\nES_SNAPSHOT_TAR_NAME=\"es_snapshot_${SNAPSHOT_HEIGHT}.tar.gz\"\n\nSNAPSHOT_URL=\"https://snapshots.lbry.com/hub/${SNAPSHOT_TAR_NAME}\"\nES_SNAPSHOT_URL=\"https://snapshots.lbry.com/hub/${ES_SNAPSHOT_TAR_NAME}\"\n\necho \"fetching wallet server snapshot\"\nwget $SNAPSHOT_URL\necho \"decompressing wallet server snapshot\"\ntar -xf $SNAPSHOT_TAR_NAME\nsudo mkdir -p $HUB_VOLUME_PATH\nsudo rm -rf \"${HUB_VOLUME_PATH}/_data\"\nsudo chown -R 999:999 \"snapshot_${SNAPSHOT_HEIGHT}\"\nsudo mv \"snapshot_${SNAPSHOT_HEIGHT}\" \"${HUB_VOLUME_PATH}/_data\"\necho \"finished setting up wallet server snapshot\"\n\necho \"fetching elasticsearch snapshot\"\nwget $ES_SNAPSHOT_URL\necho \"decompressing elasticsearch snapshot\"\ntar -xf $ES_SNAPSHOT_TAR_NAME\nsudo chown -R $USER:root \"snapshot_es_${SNAPSHOT_HEIGHT}\"\nsudo chmod -R 775 \"snapshot_es_${SNAPSHOT_HEIGHT}\"\nsudo mkdir -p $ES_VOLUME_PATH\nsudo rm -rf \"${ES_VOLUME_PATH}/_data\"\nsudo mv \"snapshot_es_${SNAPSHOT_HEIGHT}\" \"${ES_VOLUME_PATH}/_data\"\necho \"finished setting up elasticsearch snapshot\"\n"
  },
  {
    "path": "scripts/monitor_slow_queries.py",
    "content": "import os, asyncio, aiohttp, json, slack, sqlparse\n\n\nasync def listen(slack_client, url):\n    async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(3)) as session:\n        print(f\"connecting to {url}\")\n        try:\n            ws = await session.ws_connect(url)\n        except (aiohttp.ClientConnectorError, asyncio.TimeoutError):\n            print(f\"failed to connect to {url}\")\n            return\n        print(f\"connected to {url}\")\n\n        async for msg in ws:\n            r = json.loads(msg.data)\n            try:\n                queries = r[\"api\"][\"search\"][\"interrupted_queries\"]\n            except KeyError:\n                continue\n\n            for q in queries:\n                # clean = re.sub(r\"\\s+\", \" \", q)\n                clean = sqlparse.format(q, reindent=True, keyword_case='upper')\n                print(f'{url}: {clean}')\n                response = await slack_client.chat_postMessage(\n                    username=url,\n                    icon_emoji=\":hourglass_flowing_sand:\",\n                    channel='#clubhouse-de-obscure',\n                    text=\"*Query timed out:* \" + clean\n                )\n                if not response[\"ok\"]:\n                    print(\"SLACK ERROR:\\n\", response)\n                print()\n\n\nasync def main():\n    try:\n        slack_client = slack.WebClient(token=os.environ['SLACK_TOKEN'], run_async=True)\n    except KeyError:\n        print(\"Error: SLACK_TOKEN env var required\")\n        return\n\n    num_servers = 5\n    tasks = []\n    for i in range(1, num_servers+1):\n        tasks.append(asyncio.create_task(listen(slack_client, f'http://spv{i}.lbry.com:50005')))\n    await asyncio.gather(*tasks)\n\nasyncio.run(main())\n"
  },
  {
    "path": "scripts/publish_performance.py",
    "content": "import os\nimport time\nfrom random import Random\n\nfrom pyqtgraph.Qt import QtCore, QtGui\napp = QtGui.QApplication([])\nfrom qtreactor import pyqt4reactor\npyqt4reactor.install()\n\nfrom twisted.internet import defer, task, threads\nfrom orchstr8.services import LbryServiceStack\n\nimport pyqtgraph as pg\n\n\nclass Profiler:\n    pens = [\n        (230, 25, 75),   # red\n        (60, 180, 75),   # green\n        (255, 225, 25),  # yellow\n        (0, 130, 200),   # blue\n        (245, 130, 48),  # orange\n        (145, 30, 180),  # purple\n        (70, 240, 240),  # cyan\n        (240, 50, 230),  # magenta\n        (210, 245, 60),  # lime\n        (250, 190, 190),  # pink\n        (0, 128, 128),   # teal\n    ]\n\n    def __init__(self, graph=None):\n        self.times = {}\n        self.graph = graph\n\n    def start(self, name):\n        if name in self.times:\n            self.times[name]['start'] = time.time()\n        else:\n            self.times[name] = {\n                'start': time.time(),\n                'data': [],\n                'plot': self.graph.plot(\n                    pen=self.pens[len(self.times)],\n                    symbolBrush=self.pens[len(self.times)],\n                    name=name\n                )\n            }\n\n    def stop(self, name):\n        elapsed = time.time() - self.times[name]['start']\n        self.times[name]['start'] = None\n        self.times[name]['data'].append(elapsed)\n\n    def draw(self):\n        for plot in self.times.values():\n            plot['plot'].setData(plot['data'])\n\n\nclass ThePublisherOfThings:\n\n    def __init__(self, blocks=100, txns_per_block=100, seed=2015, start_blocks=110):\n        self.blocks = blocks\n        self.txns_per_block = txns_per_block\n        self.start_blocks = start_blocks\n        self.random = Random(seed)\n        self.profiler = Profiler()\n        self.service = LbryServiceStack(verbose=True, profiler=self.profiler)\n        self.publish_file = None\n\n    @defer.inlineCallbacks\n    def start(self):\n        yield self.service.startup(\n            after_lbrycrd_start=lambda: self.service.lbrycrd.generate(1010)\n        )\n        wallet = self.service.lbry.wallet\n        address = yield wallet.get_least_used_address()\n        sendtxid = yield self.service.lbrycrd.sendtoaddress(address, 100)\n        yield self.service.lbrycrd.generate(1)\n        yield wallet.wait_for_tx_in_wallet(sendtxid)\n        yield wallet.update_balance()\n        self.publish_file = os.path.join(self.service.lbry.download_directory, 'the_file')\n        with open(self.publish_file, 'w') as _publish_file:\n            _publish_file.write('message that will be heard around the world\\n')\n        yield threads.deferToThread(time.sleep, 0.5)\n\n    @defer.inlineCallbacks\n    def generate_publishes(self):\n\n        win = pg.GraphicsLayoutWidget(show=True)\n        win.setWindowTitle('orchstr8: performance monitor')\n        win.resize(1800, 600)\n\n        p4 = win.addPlot()\n        p4.addLegend()\n        p4.setDownsampling(mode='peak')\n        p4.setClipToView(True)\n        self.profiler.graph = p4\n\n        for block in range(self.blocks):\n            for txn in range(self.txns_per_block):\n                name = f'block{block}txn{txn}'\n                self.profiler.start('total')\n                yield self.service.lbry.daemon.jsonrpc_publish(\n                    name=name, bid=self.random.randrange(1, 5)/1000.0,\n                    file_path=self.publish_file, metadata={\n                        \"description\": \"Some interesting content\",\n                        \"title\": \"My interesting content\",\n                        \"author\": \"Video shot by me@example.com\",\n                        \"language\": \"en\", \"license\": \"LBRY Inc\", \"nsfw\": False\n                    }\n                )\n                self.profiler.stop('total')\n                self.profiler.draw()\n\n            yield self.service.lbrycrd.generate(1)\n\n    def stop(self):\n        return self.service.shutdown(cleanup=False)\n\n\n@defer.inlineCallbacks\ndef generate_publishes(_):\n    pub = ThePublisherOfThings(50, 10)\n    yield pub.start()\n    yield pub.generate_publishes()\n    yield pub.stop()\n    print(f'lbrycrd: {pub.service.lbrycrd.data_path}')\n    print(f'lbrynet: {pub.service.lbry.data_path}')\n    print(f'lbryumserver: {pub.service.lbryumserver.data_path}')\n\n\nif __name__ == \"__main__\":\n    task.react(generate_publishes)\n"
  },
  {
    "path": "scripts/release.py",
    "content": "import os\nimport re\nimport io\nimport sys\nimport yaml\nimport argparse\nimport unittest\nfrom datetime import date\nfrom getpass import getpass\n\ntry:\n    import github3\nexcept ImportError:\n    print('To run release tool you need to install github3.py:')\n    print('')\n    print('  $ pip install github3.py')\n    print('')\n    sys.exit(1)\n\n\nAREA_RENAME = {\n    'api': 'API',\n    'dht': 'DHT'\n}\n\n\ndef get_github():\n    config_path = os.path.expanduser('~/.config/gh/hosts.yml')\n    if os.path.exists(config_path):\n        with open(config_path, 'r') as config_file:\n            config = yaml.load(config_file, Loader=yaml.FullLoader)\n            return github3.login(token=config['github.com']['oauth_token'])\n\n    print('To run release tool you need to first login using the github cli:')\n    print('')\n    print('  $ gh auth login')\n    print('')\n    sys.exit(1)\n\n\ndef get_labels(pr, prefix):\n    for label in pr.labels:\n        label_name = label['name']\n        if label_name.startswith(f'{prefix}: '):\n            yield label_name[len(f'{prefix}: '):]\n\n\ndef get_label(pr, prefix):\n    for label in get_labels(pr, prefix):\n        return label\n\n\nBACKWARDS_INCOMPATIBLE = 'backwards-incompatible:'\nRELEASE_TEXT = 'release-text:'\nRELEASE_TEXT_LINES = 'release-text-lines:'\n\n\ndef get_backwards_incompatible(desc: str):\n    for line in desc.splitlines():\n        if line.startswith(BACKWARDS_INCOMPATIBLE):\n            yield line[len(BACKWARDS_INCOMPATIBLE):]\n\n\ndef get_release_text(desc: str):\n    in_release_lines = False\n    for line in desc.splitlines():\n        if in_release_lines:\n            yield line.rstrip()\n        elif line.startswith(RELEASE_TEXT_LINES):\n            in_release_lines = True\n        elif line.startswith(RELEASE_TEXT):\n            yield line[len(RELEASE_TEXT):].strip()\n            yield ''\n\n\nclass Version:\n\n    def __init__(self, major=0, minor=0, micro=0):\n        self.major = int(major)\n        self.minor = int(minor)\n        self.micro = int(micro)\n\n    @classmethod\n    def from_string(cls, version_string):\n        (major, minor, micro), rc = version_string.split('.'), None\n        if 'rc' in micro:\n            micro, rc = micro.split('rc')\n        return cls(major, minor, micro)\n\n    @classmethod\n    def from_content(cls, content):\n        src = content.decoded.decode('utf-8')\n        version = re.search('__version__ = \"(.*?)\"', src).group(1)\n        return cls.from_string(version)\n\n    def increment(self, action):\n        cls = self.__class__\n\n        if action == 'major':\n            return cls(self.major+1)\n        elif action == 'minor':\n            return cls(self.major, self.minor+1)\n        elif action == 'micro':\n            return cls(self.major, self.minor, self.micro+1)\n\n        raise ValueError(f'unknown action: {action}')\n\n    @property\n    def tag(self):\n        return f'v{self}'\n\n    def __str__(self):\n        return '.'.join(str(p) for p in [self.major, self.minor, self.micro])\n\n\ndef release(args):\n    gh = get_github()\n    repo = gh.repository('lbryio', 'lbry-sdk')\n    version_file = repo.file_contents('lbry/__init__.py')\n\n    if not args.confirm:\n        print(\"\\nDRY RUN ONLY. RUN WITH --confirm TO DO A REAL RELEASE.\\n\")\n\n    current_version = Version.from_content(version_file)\n    print(f'Current Version: {current_version}')\n\n    if args.action == 'current':\n        new_version = current_version\n    else:\n        new_version = current_version.increment(args.action)\n    print(f'    New Version: {new_version}')\n\n    previous_release = repo.release_from_tag(args.start_tag or current_version.tag)\n\n    print(f' Changelog From: {previous_release.tag_name} ({previous_release.created_at})')\n    print()\n\n    incompats = []\n    release_texts = []\n    unlabeled = []\n    fixups = []\n    areas = {}\n    for pr in gh.search_issues(f\"merged:>={previous_release._json_data['created_at']} repo:lbryio/lbry-sdk\"):\n        area_labels = list(get_labels(pr, 'area'))\n        type_label = get_label(pr, 'type')\n        pr_url = f'[#{pr.number}]({pr.html_url})'\n        user_url = f'[{pr.user[\"login\"]}]({pr.user[\"html_url\"]})'\n        if area_labels and type_label:\n            for area_name in area_labels:\n                for incompat in get_backwards_incompatible(pr.body or \"\"):\n                    incompats.append(f'  * [{area_name}] {incompat.strip()} ({pr_url})')\n                for release_text in get_release_text(pr.body or \"\"):\n                    release_texts.append(release_text)\n                if type_label == 'fixup':\n                    fixups.append(f'  * {pr.title} ({pr_url}) by {user_url}')\n                else:\n                    area = areas.setdefault(area_name, [])\n                    area.append(f'  * [{type_label}] {pr.title} ({pr_url}) by {user_url}')\n        else:\n            unlabeled.append(f'  * {pr.title} ({pr_url}) by {user_url}')\n\n    area_names = list(areas.keys())\n    area_names.sort()\n\n    body = io.StringIO()\n    w = lambda s: body.write(s+'\\n')\n\n    w(f'## [{new_version}] - {date.today().isoformat()}')\n    if release_texts:\n        w('')\n        for release_text in release_texts:\n            w(release_text)\n    if incompats:\n        w('')\n        w(f'### Backwards Incompatible Changes')\n        for incompat in incompats:\n            w(incompat)\n    for area in area_names:\n        prs = areas[area]\n        area = AREA_RENAME.get(area.lower(), area.capitalize())\n        w('')\n        w(f'### {area}')\n        for pr in prs:\n            w(pr)\n\n    print(body.getvalue())\n\n    if unlabeled:\n        print('The following PRs were skipped and not included in changelog:')\n        for skipped in unlabeled:\n            print(skipped)\n\n    if fixups:\n        print('The following PRs were marked as fixups and not included in changelog:')\n        for skipped in fixups:\n            print(skipped)\n\n    if args.confirm:\n\n        commit = version_file.update(\n          new_version.tag,\n          version_file.decoded.decode('utf-8').replace(str(current_version), str(new_version)).encode()\n        )['commit']\n\n        repo.create_tag(\n            tag=new_version.tag,\n            message=new_version.tag,\n            sha=commit.sha,\n            obj_type='commit',\n            tagger=commit.committer\n        )\n\n        repo.create_release(\n            new_version.tag,\n            name=new_version.tag,\n            body=body.getvalue(),\n            draft=True,\n        )\n\n    return 0\n\n\nclass TestReleaseTool(unittest.TestCase):\n\n    def test_version_parsing(self):\n        self.assertTrue(str(Version.from_string('1.2.3')), '1.2.3')\n        self.assertTrue(str(Version.from_string('1.2.3rc4')), '1.2.3rc4')\n\n    def test_version_increment(self):\n        v = Version.from_string('1.2.3')\n        self.assertTrue(str(v.increment('major')), '2.0.0')\n        self.assertTrue(str(v.increment('minor')), '1.3.0')\n        self.assertTrue(str(v.increment('micro')), '1.2.4')\n\n\ndef test():\n    runner = unittest.TextTestRunner(verbosity=2)\n    loader = unittest.TestLoader()\n    suite = loader.loadTestsFromTestCase(TestReleaseTool)\n    return 0 if runner.run(suite).wasSuccessful() else 1\n\n\ndef main():\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--confirm\", default=False, action=\"store_true\",\n                        help=\"without this flag, it will only print what it will do but will not actually do it\")\n    parser.add_argument(\"--start-tag\", help=\"custom starting tag for changelog generation\")\n    parser.add_argument(\"action\", choices=['test', 'current', 'major', 'minor', 'micro'])\n    args = parser.parse_args()\n\n    if args.action == \"test\":\n        code = test()\n    else:\n        code = release(args)\n\n    print()\n    return code\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": "scripts/repair_0_31_1_db.py",
    "content": "import os\nimport binascii\nimport sqlite3\nfrom lbry.conf import Config\n\n\ndef main():\n    conf = Config()\n    db = sqlite3.connect(os.path.join(conf.data_dir, 'lbrynet.sqlite'))\n    cur = db.cursor()\n    files = cur.execute(\"select stream_hash, file_name, download_directory from file\").fetchall()\n    update = {}\n    for stream_hash, file_name, download_directory in files:\n        try:\n            binascii.unhexlify(file_name)\n        except binascii.Error:\n            try:\n                binascii.unhexlify(download_directory)\n            except binascii.Error:\n                update[stream_hash] = (\n                    binascii.hexlify(file_name.encode()).decode(), binascii.hexlify(download_directory.encode()).decode()\n                )\n    if update:\n        print(f\"repair {len(update)} streams\")\n        for stream_hash, (file_name, download_directory) in update.items():\n            cur.execute('update file set file_name=?, download_directory=? where stream_hash=?',\n                        (file_name, download_directory, stream_hash))\n    db.commit()\n    db.close()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/sd_hash_sampler.py",
    "content": "import asyncio\nfrom typing import Iterable\n\nfrom lbry.extras.daemon.client import daemon_rpc\nfrom lbry.conf import Config\nconf = Config()\n\n\nasync def sample_prefix(prefix: bytes):\n    result = await daemon_rpc(conf, \"claim_search\", sd_hash=prefix.hex(), page_size=50)\n    total_pages = result['total_pages']\n    print(total_pages)\n    sd_hashes = set()\n    for page in range(1, total_pages + 1):\n        if page > 1:\n            result = await daemon_rpc(conf, \"claim_search\", sd_hash=prefix.hex(), page=page, page_size=50)\n        for item in result['items']:\n            sd_hash = item.get('value', {}).get('source', {}).get('sd_hash')\n            if not sd_hash:\n                print('err', item)\n                continue\n            sd_hashes.add(sd_hash)\n        print('page', page, len(sd_hashes))\n    return sd_hashes\n\n\ndef save_sample(name: str, samples: Iterable[str]):\n    with open(name, 'wb') as outfile:\n        for sample in samples:\n            outfile.write(bytes.fromhex(sample))\n        outfile.flush()\n        print(outfile.tell())\n\n\nasync def main():\n    samples = set()\n    futs = [asyncio.ensure_future(sample_prefix(bytes([i]))) for i in range(256)]\n    for i, completed in enumerate(asyncio.as_completed(futs)):\n        samples.update(await completed)\n        print(i, len(samples))\n    print(save_sample(\"test.sample\", samples))\n\nif __name__ == \"__main__\":\n    asyncio.run(main())"
  },
  {
    "path": "scripts/standalone_blob_server.py",
    "content": "import sys\nimport os\nimport asyncio\nfrom lbry.blob.blob_manager import BlobManager\nfrom lbry.blob_exchange.server import BlobServer\nfrom lbry.schema.address import decode_address\nfrom lbry.extras.daemon.storage import SQLiteStorage\n\n\nasync def main(address: str):\n    try:\n        decode_address(address)\n    except:\n        print(f\"'{address}' is not a valid lbrycrd address\")\n        return 1\n    loop = asyncio.get_running_loop()\n\n    storage = SQLiteStorage(os.path.expanduser(\"~/.lbrynet/lbrynet.sqlite\"))\n    await storage.open()\n    blob_manager = BlobManager(loop, os.path.expanduser(\"~/.lbrynet/blobfiles\"), storage)\n    await blob_manager.setup()\n\n    server = await loop.create_server(\n        lambda: BlobServer(loop, blob_manager, address),\n        '0.0.0.0', 4444)\n    try:\n        async with server:\n            await server.serve_forever()\n    finally:\n        await storage.close()\n\nif __name__ == \"__main__\":\n    asyncio.run(main(sys.argv[1]))\n"
  },
  {
    "path": "scripts/test_claim_search.py",
    "content": "import asyncio\nfrom lbry.wallet.network import ClientSession\nfrom lbry.wallet.rpc.jsonrpc import RPCError\nimport logging\nimport json\nimport sys\n\nlogging.getLogger('lbry.wallet').setLevel(logging.CRITICAL)\n\n\nasync def main():\n    try:\n        hostname = sys.argv[1]\n    except IndexError:\n        hostname = 'spv11.lbry.com'\n\n    loop = asyncio.get_event_loop()\n    client = ClientSession(network=None, server=(hostname, 50001))\n    error = None\n    args = {\n        'any_tags': ['art'],\n        'not_tags': ['xxx', 'porn', 'mature', 'nsfw', 'titan'],\n        'order_by': [\"name\"],\n        'offset': 3000,\n        'limit': 200,\n        'no_totals': False,\n    }\n\n    start = loop.time()\n    try:\n        await client.create_connection()\n        try:\n            await client.send_request('blockchain.claimtrie.search', args)\n        except RPCError as err:\n            error = err\n        finally:\n            await client.close()\n    finally:\n        print(json.dumps({\n            \"time\": loop.time() - start,\n            \"error\": error.__str__() if error else None,\n            \"args\": args,\n        }, indent=4))\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "scripts/time_to_first_byte.py",
    "content": "import os\nimport sys\nimport json\nimport argparse\nimport asyncio\nimport time\n\nimport aiohttp\nfrom aiohttp import ClientConnectorError\nfrom lbry import __version__\nfrom lbry.blob.blob_file import MAX_BLOB_SIZE\nfrom lbry.conf import Config\nfrom lbry.extras.daemon.client import daemon_rpc\nfrom lbry.extras import system_info\n\n\nasync def report_to_slack(output, webhook):\n    payload = {\n        \"text\": f\"lbrynet {__version__} ({system_info.get_platform()['platform']}) time to first byte:\\n{output}\"\n    }\n    async with aiohttp.request('post', webhook, data=json.dumps(payload)):\n        pass\n\n\ndef confidence(times, z, plus_err=True):\n    mean = sum(times) / len(times)\n    standard_dev = (sum((t - sum(times) / len(times)) ** 2.0 for t in times) / len(times)) ** 0.5\n    err = (z * standard_dev) / (len(times) ** 0.5)\n    return f\"{round((mean + err) if plus_err else (mean - err), 3)}\"\n\n\ndef variance(times):\n    mean = sum(times) / len(times)\n    return round(sum((i - mean) ** 2.0 for i in times) / (len(times) - 1), 3)\n\n\nasync def wait_for_done(conf, claim_name, timeout):\n    blobs_completed, last_completed = 0, time.perf_counter()\n    while True:\n        file = (await daemon_rpc(conf, \"file_list\", claim_name=claim_name))['items'][0]\n        if file['status'] in ['finished', 'stopped']:\n            return True, file['blobs_completed'], file['blobs_in_stream']\n        elif blobs_completed < int(file['blobs_completed']):\n            blobs_completed, last_completed = int(file['blobs_completed']), time.perf_counter()\n        elif (time.perf_counter() - last_completed) > timeout:\n            return False, file['blobs_completed'], file['blobs_in_stream']\n        await asyncio.sleep(1.0)\n\n\nasync def main(cmd_args=None):\n    print('Time to first byte started using parameters:')\n    for key, value in vars(cmd_args).items():\n        print(f\"{key}: {value}\")\n    conf = Config()\n    url_to_claim = {}\n    try:\n        for page in range(1, cmd_args.download_pages + 1):\n            start = time.perf_counter()\n            kwargs = {\n                'page': page,\n                'claim_type': 'stream',\n                'any_tags': [\n                  'art',\n                  'automotive',\n                  'blockchain',\n                  'comedy',\n                  'economics',\n                  'education',\n                  'gaming',\n                  'music',\n                  'news',\n                  'science',\n                  'sports',\n                  'technology',\n                ],\n                'order_by': ['trending_global', 'trending_mixed'],\n                'no_totals': True\n            }\n\n            if not cmd_args.allow_fees:\n                kwargs['fee_amount'] = 0\n\n            response = await daemon_rpc(\n                conf, 'claim_search', **kwargs\n            )\n            if 'error' in response or not response.get('items'):\n                print(f'Error getting claim list page {page}:')\n                print(response)\n                return 1\n            else:\n                url_to_claim.update({\n                    claim['permanent_url']: claim for claim in response['items'] if claim['value_type'] == 'stream'\n                })\n            print(f'Claim search page {page} took: {time.perf_counter() - start}')\n    except (ClientConnectorError, ConnectionError):\n        print(\"Could not connect to daemon\")\n        return 1\n    print(\"**********************************************\")\n    print(f\"Attempting to download {len(url_to_claim)} claim_search streams\")\n\n    first_byte_times = []\n    download_speeds = []\n    download_successes = []\n    failed_to = {}\n\n    await asyncio.gather(*(\n        daemon_rpc(conf, 'file_delete', delete_from_download_dir=True, claim_name=claim['name'])\n        for claim in url_to_claim.values() if not cmd_args.keep_files\n    ))\n\n    for i, (url, claim) in enumerate(url_to_claim.items()):\n        start = time.perf_counter()\n        response = await daemon_rpc(conf, 'get', uri=url, save_file=not cmd_args.head_blob_only)\n        if 'error' in response:\n            print(f\"{i + 1}/{len(url_to_claim)} - failed to start {url}: {response['error']}\")\n            failed_to[url] = 'start'\n            if cmd_args.exit_on_error:\n                return\n            continue\n        first_byte = time.perf_counter()\n        first_byte_times.append(first_byte - start)\n        print(f\"{i + 1}/{len(url_to_claim)} - {first_byte - start} {url}\")\n        if not cmd_args.head_blob_only:\n            downloaded, amount_downloaded, blobs_in_stream = await wait_for_done(\n                conf, claim['name'], cmd_args.stall_download_timeout\n            )\n            if downloaded:\n                download_successes.append(url)\n            else:\n                failed_to[url] = 'finish'\n            mbs = round((blobs_in_stream * (MAX_BLOB_SIZE - 1)) / (time.perf_counter() - start) / 1000000, 2)\n            download_speeds.append(mbs)\n            print(f\"downloaded {amount_downloaded}/{blobs_in_stream} blobs for {url} at \"\n                  f\"{mbs}mb/s\")\n        if not cmd_args.keep_files:\n            await daemon_rpc(conf, 'file_delete', delete_from_download_dir=True, claim_name=claim['name'])\n        await asyncio.sleep(0.1)\n\n    print(\"**********************************************\")\n    result = f\"Started {len(first_byte_times)} of {len(url_to_claim)} attempted front page streams\\n\"\n    if first_byte_times:\n        result += f\"Worst first byte time: {round(max(first_byte_times), 2)}\\n\" \\\n                  f\"Best first byte time: {round(min(first_byte_times), 2)}\\n\" \\\n                  f\"*95% confidence time-to-first-byte: {confidence(first_byte_times, 1.984)}s*\\n\" \\\n                  f\"99% confidence time-to-first-byte:  {confidence(first_byte_times, 2.626)}s\\n\" \\\n                  f\"Variance: {variance(first_byte_times)}\\n\"\n    if download_successes:\n        result += f\"Downloaded {len(download_successes)}/{len(url_to_claim)}\\n\" \\\n                  f\"Best stream download speed: {round(max(download_speeds), 2)}mb/s\\n\" \\\n                  f\"Worst stream download speed: {round(min(download_speeds), 2)}mb/s\\n\" \\\n                  f\"95% confidence download speed: {confidence(download_speeds, 1.984, False)}mb/s\\n\" \\\n                  f\"99% confidence download speed:  {confidence(download_speeds, 2.626, False)}mb/s\\n\"\n\n    for reason in ('start', 'finish'):\n        failures = [url for url, why in failed_to.items() if reason == why]\n        if failures:\n            result += f\"\\nFailed to {reason}:\\n\" + \"\\n•\".join(failures)\n    print(result)\n\n    webhook = os.environ.get('TTFB_SLACK_TOKEN', None)\n    if webhook:\n        await report_to_slack(result, webhook)\n    return 0\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--allow_fees\", action='store_true')\n    parser.add_argument(\"--exit_on_error\", action='store_true')\n    parser.add_argument(\"--stall_download_timeout\", default=5, type=int)\n    parser.add_argument(\"--keep_files\", action='store_true')\n    parser.add_argument(\"--head_blob_only\", action='store_true')\n    parser.add_argument(\"--download_pages\", type=int, default=10)\n    sys.exit(asyncio.run(main(cmd_args=parser.parse_args())) or 0)\n"
  },
  {
    "path": "scripts/troubleshoot_p2p_and_dht_webservice.py",
    "content": "import asyncio\nfrom aiohttp import web\n\nfrom lbry.blob_exchange.serialization import BlobRequest, BlobResponse\nfrom lbry.dht.constants import generate_id\nfrom lbry.dht.node import Node\nfrom lbry.dht.peer import make_kademlia_peer, PeerManager\nfrom lbry.extras.daemon.storage import SQLiteStorage\n\nloop = asyncio.get_event_loop()\nNODE = Node(\n    loop, PeerManager(loop), generate_id(), 60600, 60600, 3333, None,\n    storage=SQLiteStorage(None, \":memory:\", loop, loop.time)\n)\n\n\nasync def check_p2p(ip, port):\n    writer = None\n    try:\n        reader, writer = await asyncio.open_connection(ip, port)\n        writer.write(BlobRequest.make_request_for_blob_hash('0'*96).serialize())\n        return BlobResponse.deserialize(await reader.readuntil(b'}')).get_address_response().lbrycrd_address\n    except OSError:\n        return None\n    finally:\n        if writer:\n            writer.close()\n            await writer.wait_closed()\n\n\nasync def check_dht(ip, port):\n    peer = make_kademlia_peer(None, ip, udp_port=int(port))\n    return await NODE.protocol.get_rpc_peer(peer).ping()\n\n\nasync def endpoint_p2p(request):\n    p2p_port = request.match_info.get('p2p_port', \"3333\")\n    try:\n        address = await asyncio.wait_for(check_p2p(request.remote, p2p_port), 3)\n    except asyncio.TimeoutError:\n        address = None\n    return {\"status\": address is not None, \"port\": p2p_port, \"payment_address\": address}\n\n\nasync def endpoint_dht(request):\n    dht_port = request.match_info.get('dht_port', \"3333\")\n    try:\n        response = await check_dht(request.remote, dht_port)\n    except asyncio.TimeoutError:\n        response = None\n    return {\"status\": response == b'pong', \"port\": dht_port}\n\n\nasync def endpoint_default(request):\n    return {\"dht_status\": await endpoint_dht(request), \"p2p_status\": await endpoint_p2p(request)}\n\n\ndef as_json_response_wrapper(endpoint):\n    async def json_endpoint(*args, **kwargs):\n        return web.json_response(await endpoint(*args, **kwargs))\n    return json_endpoint\n\n\napp = web.Application()\napp.add_routes([web.get('/', as_json_response_wrapper(endpoint_default)),\n                web.get('/dht/{dht_port}', as_json_response_wrapper(endpoint_dht)),\n                web.get('/p2p/{p2p_port}', as_json_response_wrapper(endpoint_p2p))])\n\nif __name__ == '__main__':\n    loop.create_task(NODE.start_listening(\"0.0.0.0\"))\n    web.run_app(app, port=60666)"
  },
  {
    "path": "scripts/wallet_server_monitor.py",
    "content": "import sys\nimport json\nimport random\nimport asyncio\nimport argparse\nimport traceback\nimport signal\nfrom time import time\nfrom datetime import datetime\n\ntry:\n    import aiohttp\n    import psycopg2\n    import slack\nexcept ImportError:\n    print(f\"To run {sys.argv[0]} you need to install aiohttp, psycopg2 and slackclient:\")\n    print(f\"\")\n    print(f\"  $ pip install aiohttp psycopg2 slackclient\")\n    print(\"\")\n    sys.exit(1)\n\nif not sys.version_info >= (3, 7):\n    print(\"Please use Python 3.7 or higher, this script expects that dictionary keys preserve order.\")\n    sys.exit(1)\n\n\nasync def handle_slow_query(cursor, server, command, queries):\n    for query in queries:\n        cursor.execute(\"\"\"\n        INSERT INTO wallet_server_slow_queries (server, command, query, event_time) VALUES (%s,%s,%s,%s);\n        \"\"\", (server, command, query, datetime.now()))\n\n\nasync def handle_analytics_event(cursor, event, server):\n    cursor.execute(\"\"\"\n    INSERT INTO wallet_server_stats (server, sessions, event_time) VALUES (%s,%s,%s);\n    \"\"\", (server, event['status']['sessions'], datetime.now()))\n\n    for command, stats in event[\"api\"].items():\n        data = {\n            'server': server,\n            'command': command,\n            'event_time': datetime.now()\n        }\n        for key, value in stats.items():\n            if key.endswith(\"_queries\"):\n                if key == \"interrupted_queries\":\n                    await handle_slow_query(cursor, server, command, value)\n                continue\n            if isinstance(value, list):\n                data.update({\n                    key + '_avg': value[0],\n                    key + '_min': value[1],\n                    key + '_five': value[2],\n                    key + '_twenty_five': value[3],\n                    key + '_fifty': value[4],\n                    key + '_seventy_five': value[5],\n                    key + '_ninety_five': value[6],\n                    key + '_max': value[7],\n                })\n            else:\n                data[key] = value\n\n        cursor.execute(f\"\"\"\n        INSERT INTO wallet_server_command_stats ({','.join(data)})\n        VALUES ({','.join('%s' for _ in data)});\n        \"\"\", list(data.values()))\n\n\nSLACKCLIENT = None\n\n\nasync def boris_says(what_boris_says):\n    if SLACKCLIENT:\n        await SLACKCLIENT.chat_postMessage(\n            username=\"boris the wallet monitor\",\n            icon_emoji=\":boris:\",\n            channel='#tech-sdk',\n            text=what_boris_says\n        )\n    else:\n        print(what_boris_says)\n\n\nasync def monitor(db, server):\n    c = db.cursor()\n    delay = 30\n    height_changed = None, time()\n    height_change_reported = False\n    first_attempt = True\n    while True:\n        try:\n            async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(10)) as session:\n                try:\n                    ws = await session.ws_connect(server)\n                except (aiohttp.ClientConnectionError, asyncio.TimeoutError):\n                    if first_attempt:\n                        print(f\"failed connecting to {server}\")\n                        await boris_says(random.choice([\n                            f\"{server} is not responding, probably dead, will not connect again.\",\n                        ]))\n                        return\n                    raise\n\n                if first_attempt:\n                    await boris_says(f\"{server} is online\")\n                else:\n                    await boris_says(f\"{server} is back online\")\n\n                delay = 30\n                first_attempt = False\n                print(f\"connected to {server}\")\n\n                async for msg in ws:\n                    event = json.loads(msg.data)\n                    height = event['status'].get('height')\n                    height_change_time = int(time()-height_changed[1])\n                    if height is None:\n                        pass\n                    elif height_changed[0] != height:\n                        height_changed = (height, time())\n                        if height_change_reported:\n                            await boris_says(\n                                f\"Server {server} received new block after {height_change_time / 60:.1f} minutes.\",\n                            )\n                            height_change_reported = False\n                    elif height_change_time > 30*60:\n                        if not height_change_reported or height_change_time % (2*60) == 0:\n                            await boris_says(\n                                f\"It's been {height_change_time/60:.1f} minutes since {server} received a new block.\",\n                            )\n                            height_change_reported = True\n                    await handle_analytics_event(c, event, server)\n                    db.commit()\n\n        except (aiohttp.ClientConnectionError, asyncio.TimeoutError):\n            await boris_says(random.choice([\n                f\"<!channel> Guys, we have a problem! Nobody home at {server}. Will check on it again in {delay} seconds.\",\n                f\"<!channel> Something wrong with {server}. I think dead. Will poke it again in {delay} seconds.\",\n                f\"<!channel> Don't hear anything from {server}, maybe dead. Will try it again in {delay} seconds.\",\n            ]))\n            await asyncio.sleep(delay)\n            delay += 30\n\n\nasync def main(dsn, servers):\n    db = ensure_database(dsn)\n    await boris_says(random.choice([\n        \"No fear, Boris is here! I will monitor the servers now and will try not to fall asleep again.\",\n        \"Comrad the Cat and Boris are here now, monitoring wallet servers.\",\n    ]))\n    await asyncio.gather(*(\n        asyncio.create_task(monitor(db, server))\n        for server in servers\n    ))\n\n\ndef ensure_database(dsn):\n    db = psycopg2.connect(**dsn)\n    c = db.cursor()\n\n    c.execute(\"SELECT to_regclass('wallet_server_stats');\")\n    if c.fetchone()[0] is None:\n        print(\"creating table 'wallet_server_stats'...\")\n        c.execute(\"\"\"\n        CREATE TABLE wallet_server_stats (\n            server text,\n            sessions integer,\n            event_time timestamp\n        );\n        \"\"\")\n\n    c.execute(\"SELECT to_regclass('wallet_server_slow_queries');\")\n    if c.fetchone()[0] is None:\n        print(\"creating table 'wallet_server_slow_queries'...\")\n        c.execute(\"\"\"\n        CREATE TABLE wallet_server_slow_queries (\n            server text,\n            command text,\n            query text,\n            event_time timestamp\n        );\n        \"\"\")\n\n    c.execute(\"SELECT to_regclass('wallet_server_command_stats');\")\n    if c.fetchone()[0] is None:\n        print(\"creating table 'wallet_server_command_stats'...\")\n        c.execute(\"\"\"\n        CREATE TABLE wallet_server_command_stats (\n            server text,\n            command text,\n            event_time timestamp,\n\n            -- total requests received during event window\n            receive_count integer,\n\n            -- sum of these is total responses made\n            cache_response_count integer,\n            query_response_count integer,\n            intrp_response_count integer,\n            error_response_count integer,\n\n            -- millisecond timings for non-cache responses (response_*, interrupt_*, error_*)\n\n            response_avg float,\n            response_min float,\n            response_five float,\n            response_twenty_five float,\n            response_fifty float,\n            response_seventy_five float,\n            response_ninety_five float,\n            response_max float,\n\n            interrupt_avg float,\n            interrupt_min float,\n            interrupt_five float,\n            interrupt_twenty_five float,\n            interrupt_fifty float,\n            interrupt_seventy_five float,\n            interrupt_ninety_five float,\n            interrupt_max float,\n\n            error_avg float,\n            error_min float,\n            error_five float,\n            error_twenty_five float,\n            error_fifty float,\n            error_seventy_five float,\n            error_ninety_five float,\n            error_max float,\n\n            -- response, interrupt and error each also report the python, wait and sql stats\n\n            python_avg float,\n            python_min float,\n            python_five float,\n            python_twenty_five float,\n            python_fifty float,\n            python_seventy_five float,\n            python_ninety_five float,\n            python_max float,\n\n            wait_avg float,\n            wait_min float,\n            wait_five float,\n            wait_twenty_five float,\n            wait_fifty float,\n            wait_seventy_five float,\n            wait_ninety_five float,\n            wait_max float,\n\n            sql_avg float,\n            sql_min float,\n            sql_five float,\n            sql_twenty_five float,\n            sql_fifty float,\n            sql_seventy_five float,\n            sql_ninety_five float,\n            sql_max float,\n\n            -- extended timings for individual sql executions\n            individual_sql_avg float,\n            individual_sql_min float,\n            individual_sql_five float,\n            individual_sql_twenty_five float,\n            individual_sql_fifty float,\n            individual_sql_seventy_five float,\n            individual_sql_ninety_five float,\n            individual_sql_max float,\n\n            individual_sql_count integer\n        );\n        \"\"\")\n        db.commit()\n    return db\n\n\ndef get_dsn(args):\n    dsn = {}\n    for attr in ('dbname', 'user', 'password', 'host', 'port'):\n        value = getattr(args, f'pg_{attr}')\n        if value:\n            dsn[attr] = value\n    return dsn\n\n\ndef get_servers(args):\n    servers = []\n    for s in args.server_range.split(\",\"):\n        if '..' in s:\n            start, end = s.split('..')\n            servers.extend(range(int(start), int(end)+1))\n        else:\n            servers.append(int(s))\n    return [args.server_url.format(i) for i in servers]\n\n\ndef get_slack_client(args):\n    if args.slack_token:\n        return slack.WebClient(token=args.slack_token, run_async=True)\n\n\ndef get_args():\n    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)\n    parser.add_argument(\"--pg-dbname\", default=\"analytics\", help=\"PostgreSQL database name\")\n    parser.add_argument(\"--pg-user\", help=\"PostgreSQL username\")\n    parser.add_argument(\"--pg-password\", help=\"PostgreSQL password\")\n    parser.add_argument(\"--pg-host\", default=\"localhost\", help=\"PostgreSQL host\")\n    parser.add_argument(\"--pg-port\", default=\"5432\", help=\"PostgreSQL port\")\n    parser.add_argument(\"--server-url\", default=\"http://spv{}.lbry.com:50005\", help=\"URL with '{}' placeholder\")\n    parser.add_argument(\"--server-range\", default=\"1..5\", help=\"Range of numbers or single number to use in URL placeholder\")\n    parser.add_argument(\"--slack-token\")\n    return parser.parse_args()\n\n\nasync def shutdown(signal, loop):\n    await boris_says(f\"I got signal {signal.name}. Shutting down.\")\n    tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]\n    [task.cancel() for task in tasks]\n    await asyncio.gather(*tasks, return_exceptions=True)\n    # loop.stop()\n\n\nif __name__ == \"__main__\":\n    loop = asyncio.get_event_loop()\n    for sig in (signal.SIGHUP, signal.SIGTERM, signal.SIGINT):\n        loop.add_signal_handler(sig, lambda s=sig: asyncio.create_task(shutdown(s, loop)))\n\n    args = get_args()\n    SLACKCLIENT = get_slack_client(args)\n    try:\n        loop.run_until_complete(main(get_dsn(args), get_servers(args)))\n    except asyncio.CancelledError as e:\n        pass\n    except Exception as e:\n        loop.run_until_complete(boris_says(\"<!channel> I crashed with the following exception:\"))\n        loop.run_until_complete(boris_says(traceback.format_exc()))\n    finally:\n        loop.run_until_complete(\n            boris_says(random.choice([\n                \"Wallet servers will have to watch themselves, I'm leaving now.\",\n                \"I'm going to go take a nap, hopefully nothing blows up while I'm gone.\",\n                \"Babushka is calling, I'll be back later, someone else watch the servers while I'm gone.\",\n            ]))\n        )\n"
  },
  {
    "path": "setup.cfg",
    "content": "[coverage:run]\nbranch = True\n\n[coverage:paths]\nsource =\n  lbry\n  .tox/*/lib/python*/site-packages/lbry\nomit =\n  lbry/wallet/orchstr8/\n  .tox/*/lib/python*/site-packages/lbry/wallet/orchstr8/node.py\n\n[cryptography.*,coincurve.*,pbkdf2,libtorrent]\nignore_missing_imports = True\n\n[pylint]\njobs=8\nignore=words,server,rpc,schema,winpaths.py,migrator,undecorated.py\nmax-parents=10\nmax-args=10\nmax-line-length=120\ngood-names=T,t,n,i,j,k,x,y,s,f,d,h,c,e,op,db,tx,io,cachedproperty,log,id,r,iv,ts,l,pk\nvalid-metaclass-classmethod-first-arg=mcs\ndisable=\n  c-extension-no-member,\n  fixme,\n  broad-except,\n  raise-missing-from,\n  no-else-return,\n  cyclic-import,\n  missing-docstring,\n  consider-using-f-string,\n  duplicate-code,\n  expression-not-assigned,\n  inconsistent-return-statements,\n  too-few-public-methods,\n  too-many-lines,\n  too-many-locals,\n  too-many-branches,\n  too-many-arguments,\n  too-many-statements,\n  too-many-nested-blocks,\n  too-many-public-methods,\n  too-many-return-statements,\n  too-many-instance-attributes,\n  unspecified-encoding,\n  protected-access,\n  unused-argument\n"
  },
  {
    "path": "setup.py",
    "content": "import os\nimport sys\nfrom lbry import __name__, __version__\nfrom setuptools import setup, find_packages\n\nBASE = os.path.dirname(__file__)\nwith open(os.path.join(BASE, 'README.md'), encoding='utf-8') as fh:\n    long_description = fh.read()\n\nsetup(\n    name=__name__,\n    version=__version__,\n    author=\"LBRY Inc.\",\n    author_email=\"hello@lbry.com\",\n    url=\"https://lbry.com\",\n    description=\"A decentralized media library and marketplace\",\n    long_description=long_description,\n    long_description_content_type=\"text/markdown\",\n    keywords=\"lbry protocol media\",\n    license='MIT',\n    python_requires='>=3.8',\n    packages=find_packages(exclude=('tests',)),\n    zip_safe=False,\n    entry_points={\n        'console_scripts': [\n            'lbrynet=lbry.extras.cli:main',\n            'orchstr8=lbry.wallet.orchstr8.cli:main'\n        ],\n    },\n    install_requires=[\n        'aiohttp==3.7.4',\n        'aioupnp==0.0.18',\n        'appdirs==1.4.3',\n        'certifi>=2021.10.08',\n        'colorama==0.3.7',\n        'distro==1.4.0',\n        'base58==1.0.0',\n        'cffi==1.13.2',\n        'cryptography==3.4.7',\n        'protobuf==3.17.2',\n        'prometheus_client==0.7.1',\n        'ecdsa==0.13.3',\n        'pyyaml==5.3.1',\n        'docopt==0.6.2',\n        'hachoir==3.1.2',\n        'coincurve==15.0.0',\n        'pbkdf2==1.3',\n        'filetype==1.0.9',\n        'libtorrent==2.0.6',\n    ],\n    extras_require={\n        'lint': [\n            'pylint==2.13.9'\n        ],\n        'test': [\n            'coverage',\n            'jsonschema==4.4.0',\n        ],\n        'hub': [\n            'hub@git+https://github.com/lbryio/hub.git@929448d64bcbe6c5e476757ec78456beaa85e56a'\n        ]\n    },\n    classifiers=[\n        'Framework :: AsyncIO',\n        'Intended Audience :: Developers',\n        'Intended Audience :: System Administrators',\n        'License :: OSI Approved :: MIT License',\n        'Programming Language :: Python :: 3',\n        'Operating System :: OS Independent',\n        'Topic :: Internet',\n        'Topic :: Software Development :: Testing',\n        'Topic :: Software Development :: Libraries :: Python Modules',\n        'Topic :: System :: Distributed Computing',\n        'Topic :: Utilities',\n    ],\n)\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/dht_mocks.py",
    "content": "import typing\nimport contextlib\nimport socket\nfrom unittest import mock\nimport functools\nimport asyncio\nif typing.TYPE_CHECKING:\n    from lbry.dht.protocol.protocol import KademliaProtocol\n\n\ndef get_time_accelerator(loop: asyncio.AbstractEventLoop,\n                         instant_step: bool = False) -> typing.Callable[[float], typing.Awaitable[None]]:\n    \"\"\"\n    Returns an async advance() function\n\n    This provides a way to advance() the BaseEventLoop.time for the scheduled TimerHandles\n    made by call_later, call_at, and call_soon.\n    \"\"\"\n\n    original = loop.time\n    _drift = 0\n    loop.time = functools.wraps(loop.time)(lambda: original() + _drift)\n\n    async def accelerate_time(seconds: float) -> None:\n        nonlocal _drift\n        if seconds < 0:\n            raise ValueError(f'Cannot go back in time ({seconds} seconds)')\n        _drift += seconds\n        await asyncio.sleep(0)\n\n    async def accelerator(seconds: float):\n        steps = seconds * 10.0 if not instant_step else 1\n\n        for _ in range(max(int(steps), 1)):\n            await accelerate_time(seconds/steps)\n\n    return accelerator\n\n\n@contextlib.contextmanager\ndef mock_network_loop(loop: asyncio.AbstractEventLoop,\n                      dht_network: typing.Optional[typing.Dict[typing.Tuple[str, int], 'KademliaProtocol']] = None):\n    dht_network: typing.Dict[typing.Tuple[str, int], 'KademliaProtocol'] = dht_network if dht_network is not None else {}\n\n    async def create_datagram_endpoint(proto_lam: typing.Callable[[], 'KademliaProtocol'],\n                                       from_addr: typing.Tuple[str, int]):\n        def sendto(data, to_addr):\n            rx = dht_network.get(to_addr)\n            if rx and rx.external_ip:\n                # print(f\"{from_addr[0]}:{from_addr[1]} -{len(data)} bytes-> {rx.external_ip}:{rx.udp_port}\")\n                return rx.datagram_received(data, from_addr)\n\n        protocol = proto_lam()\n        transport = mock.Mock(spec=asyncio.DatagramTransport)\n        transport.get_extra_info = lambda k: {'socket': mock_sock}[k]\n        transport.is_closing = lambda: False\n        transport.close = lambda: mock_sock.close()\n        mock_sock.sendto = sendto\n        transport.sendto = mock_sock.sendto\n        protocol.connection_made(transport)\n        dht_network[from_addr] = protocol\n        return transport, protocol\n\n    mock_sock = mock.Mock(spec=socket.socket)\n    mock_sock.setsockopt = lambda *_: None\n    mock_sock.bind = lambda *_: None\n    mock_sock.setblocking = lambda *_: None\n    mock_sock.getsockname = lambda: \"0.0.0.0\"\n    mock_sock.getpeername = lambda: \"\"\n    mock_sock.close = lambda: None\n    mock_sock.type = socket.SOCK_DGRAM\n    mock_sock.fileno = lambda: 7\n    loop.create_datagram_endpoint = create_datagram_endpoint\n    yield\n"
  },
  {
    "path": "tests/integration/__init__.py",
    "content": ""
  },
  {
    "path": "tests/integration/blockchain/__init__.py",
    "content": ""
  },
  {
    "path": "tests/integration/blockchain/test_account_commands.py",
    "content": "from binascii import hexlify, unhexlify\n\nfrom lbry.testcase import CommandTestCase\nfrom lbry.wallet.script import InputScript\nfrom lbry.wallet.dewies import dewies_to_lbc\nfrom lbry.wallet.account import DeterministicChannelKeyManager\nfrom lbry.crypto.hash import hash160\nfrom lbry.crypto.base58 import Base58\n\n\ndef extract(d, keys):\n    return {k: d[k] for k in keys}\n\n\nclass AccountManagement(CommandTestCase):\n    async def test_account_list_set_create_remove_add(self):\n        # check initial account\n        accounts = await self.daemon.jsonrpc_account_list()\n        self.assertItemCount(accounts, 1)\n\n        # change account name and gap\n        account_id = accounts['items'][0]['id']\n        self.daemon.jsonrpc_account_set(\n            account_id=account_id, new_name='test account',\n            receiving_gap=95, receiving_max_uses=96,\n            change_gap=97, change_max_uses=98\n        )\n        accounts = (await self.daemon.jsonrpc_account_list())['items'][0]\n        self.assertEqual(accounts['name'], 'test account')\n        self.assertEqual(\n            accounts['address_generator']['receiving'],\n            {'gap': 95, 'maximum_uses_per_address': 96}\n        )\n        self.assertEqual(\n            accounts['address_generator']['change'],\n            {'gap': 97, 'maximum_uses_per_address': 98}\n        )\n\n        # create another account\n        await self.daemon.jsonrpc_account_create('second account')\n        accounts = await self.daemon.jsonrpc_account_list()\n        self.assertItemCount(accounts, 2)\n        self.assertEqual(accounts['items'][1]['name'], 'second account')\n        account_id2 = accounts['items'][1]['id']\n\n        # make new account the default\n        self.daemon.jsonrpc_account_set(account_id=account_id2, default=True)\n        accounts = await self.daemon.jsonrpc_account_list(show_seed=True)\n        self.assertEqual(accounts['items'][0]['name'], 'second account')\n\n        account_seed = accounts['items'][1]['seed']\n\n        # remove account\n        self.daemon.jsonrpc_account_remove(accounts['items'][1]['id'])\n        accounts = await self.daemon.jsonrpc_account_list()\n        self.assertItemCount(accounts, 1)\n\n        # add account\n        await self.daemon.jsonrpc_account_add('recreated account', seed=account_seed)\n        accounts = await self.daemon.jsonrpc_account_list()\n        self.assertItemCount(accounts, 2)\n        self.assertEqual(accounts['items'][1]['name'], 'recreated account')\n\n        # list specific account\n        accounts = await self.daemon.jsonrpc_account_list(account_id, include_claims=True)\n        self.assertEqual(accounts['items'][0]['name'], 'recreated account')\n\n    async def test_wallet_migration(self):\n        old_id, new_id, valid_key = (\n            'mi9E8KqFfW5ngktU22pN2jpgsdf81ZbsGY',\n            'mqs77XbdnuxWN4cXrjKbSoGLkvAHa4f4B8',\n            '-----BEGIN EC PRIVATE KEY-----\\nMHQCAQEEIBZRTZ7tHnYCH3IE9mCo95'\n            '466L/ShYFhXGrjmSMFJw8eoAcGBSuBBAAK\\noUQDQgAEmucoPz9nI+ChZrfhnh'\n            '0RZ/bcX0r2G0pYBmoNKovtKzXGa8y07D66MWsW\\nqXptakqO/9KddIkBu5eJNS'\n            'UZzQCxPQ==\\n-----END EC PRIVATE KEY-----\\n'\n        )\n        # null certificates should get deleted\n        self.account.channel_keys = {\n            new_id: 'not valid key',\n            'foo': 'bar',\n        }\n        await self.account.maybe_migrate_certificates()\n        self.assertEqual(self.account.channel_keys, {})\n        self.account.channel_keys = {\n            new_id: 'not valid key',\n            'foo': 'bar',\n            'invalid address': valid_key,\n        }\n        await self.account.maybe_migrate_certificates()\n        self.assertEqual(self.account.channel_keys, {\n            new_id: valid_key\n        })\n\n    async def assertFindsClaims(self, claim_names, awaitable):\n        self.assertEqual(claim_names, [txo.claim_name for txo in (await awaitable)['items']])\n\n    async def assertOutputAmount(self, amounts, awaitable):\n        self.assertEqual(amounts, [dewies_to_lbc(txo.amount) for txo in (await awaitable)['items']])\n\n    async def test_commands_across_accounts(self):\n        channel_list = self.daemon.jsonrpc_channel_list\n        stream_list = self.daemon.jsonrpc_stream_list\n        support_list = self.daemon.jsonrpc_support_list\n        utxo_list = self.daemon.jsonrpc_utxo_list\n        default_account = self.wallet.default_account\n        second_account = await self.daemon.jsonrpc_account_create('second account')\n\n        tx = await self.daemon.jsonrpc_account_send(\n            '0.05', await self.daemon.jsonrpc_address_unused(account_id=second_account.id), blocking=True\n        )\n        await self.confirm_tx(tx.id)\n        await self.assertOutputAmount(['0.05', '9.949876'], utxo_list())\n        await self.assertOutputAmount(['0.05'], utxo_list(account_id=second_account.id))\n        await self.assertOutputAmount(['9.949876'], utxo_list(account_id=default_account.id))\n\n        channel1 = await self.channel_create('@channel-in-account1', '0.01')\n        channel2 = await self.channel_create(\n            '@channel-in-account2', '0.01', account_id=second_account.id, funding_account_ids=[default_account.id]\n        )\n\n        await self.assertFindsClaims(['@channel-in-account2', '@channel-in-account1'], channel_list())\n        await self.assertFindsClaims(['@channel-in-account1'], channel_list(account_id=default_account.id))\n        await self.assertFindsClaims(['@channel-in-account2'], channel_list(account_id=second_account.id))\n\n        stream1 = await self.stream_create('stream-in-account1', '0.01', channel_id=self.get_claim_id(channel1))\n        stream2 = await self.stream_create(\n            'stream-in-account2', '0.01', channel_id=self.get_claim_id(channel2),\n            account_id=second_account.id, funding_account_ids=[default_account.id]\n        )\n        await self.assertFindsClaims(['stream-in-account2', 'stream-in-account1'], stream_list())\n        await self.assertFindsClaims(['stream-in-account1'], stream_list(account_id=default_account.id))\n        await self.assertFindsClaims(['stream-in-account2'], stream_list(account_id=second_account.id))\n\n        await self.assertFindsClaims(\n            ['stream-in-account2', 'stream-in-account1', '@channel-in-account2', '@channel-in-account1'],\n            self.daemon.jsonrpc_claim_list()\n        )\n        await self.assertFindsClaims(\n            ['stream-in-account1', '@channel-in-account1'],\n            self.daemon.jsonrpc_claim_list(account_id=default_account.id)\n        )\n        await self.assertFindsClaims(\n            ['stream-in-account2', '@channel-in-account2'],\n            self.daemon.jsonrpc_claim_list(account_id=second_account.id)\n        )\n\n        support1 = await self.support_create(self.get_claim_id(stream1), '0.01')\n        support2 = await self.support_create(\n            self.get_claim_id(stream2), '0.01', account_id=second_account.id, funding_account_ids=[default_account.id]\n        )\n        self.assertEqual([support2['txid'], support1['txid']], [txo.tx_ref.id for txo in (await support_list())['items']])\n        self.assertEqual([support1['txid']], [txo.tx_ref.id for txo in (await support_list(account_id=default_account.id))['items']])\n        self.assertEqual([support2['txid']], [txo.tx_ref.id for txo in (await support_list(account_id=second_account.id))['items']])\n\n        history = await self.daemon.jsonrpc_transaction_list()\n        self.assertItemCount(history, 8)\n        history = history['items']\n        self.assertEqual(extract(history[0]['support_info'][0], ['claim_name', 'is_tip', 'amount', 'balance_delta']), {\n            'claim_name': 'stream-in-account2',\n            'is_tip': False,\n            'amount': '0.01',\n            'balance_delta': '-0.01'\n        })\n        self.assertEqual(extract(history[1]['support_info'][0], ['claim_name', 'is_tip', 'amount', 'balance_delta']), {\n            'claim_name': 'stream-in-account1',\n            'is_tip': False,\n            'amount': '0.01',\n            'balance_delta': '-0.01'\n        })\n        self.assertEqual(extract(history[2]['claim_info'][0], ['claim_name', 'amount', 'balance_delta']), {\n            'claim_name': 'stream-in-account2',\n            'amount': '0.01',\n            'balance_delta': '-0.01'\n        })\n        self.assertEqual(extract(history[3]['claim_info'][0], ['claim_name', 'amount', 'balance_delta']), {\n            'claim_name': 'stream-in-account1',\n            'amount': '0.01',\n            'balance_delta': '-0.01'\n        })\n        self.assertEqual(extract(history[4]['claim_info'][0], ['claim_name', 'amount', 'balance_delta']), {\n            'claim_name': '@channel-in-account2',\n            'amount': '0.01',\n            'balance_delta': '-0.01'\n        })\n        self.assertEqual(extract(history[5]['claim_info'][0], ['claim_name', 'amount', 'balance_delta']), {\n            'claim_name': '@channel-in-account1',\n            'amount': '0.01',\n            'balance_delta': '-0.01'\n        })\n        self.assertEqual(history[6]['value'], '0.0')\n        self.assertEqual(history[7]['value'], '10.0')\n\n    async def test_address_validation(self):\n        address = await self.daemon.jsonrpc_address_unused()\n        bad_address = address[0:20] + '9999999' + address[27:]\n        with self.assertRaisesRegex(Exception, f\"'{bad_address}' is not a valid address\"):\n            await self.daemon.jsonrpc_account_send('0.1', addresses=[bad_address])\n\n    async def test_hybrid_channel_keys(self):\n        # non-deterministic channel\n        self.account.channel_keys = {\n            'mqs77XbdnuxWN4cXrjKbSoGLkvAHa4f4B8':\n                '-----BEGIN EC PRIVATE KEY-----\\nMHQCAQEEIBZRTZ7tHnYCH3IE9mCo95'\n                '466L/ShYFhXGrjmSMFJw8eoAcGBSuBBAAK\\noUQDQgAEmucoPz9nI+ChZrfhnh'\n                '0RZ/bcX0r2G0pYBmoNKovtKzXGa8y07D66MWsW\\nqXptakqO/9KddIkBu5eJNS'\n                'UZzQCxPQ==\\n-----END EC PRIVATE KEY-----\\n'\n        }\n        channel1 = await self.create_nondeterministic_channel('@foo1', '1.0', unhexlify(\n            '3056301006072a8648ce3d020106052b8104000a034200049ae7283f3f6723e0a1'\n            '66b7e19e1d1167f6dc5f4af61b4a58066a0d2a8bed2b35c66bccb4ec3eba316b16'\n            'a97a6d6a4a8effd29d748901bb9789352519cd00b13d'\n        ))\n        await self.confirm_tx(channel1['txid'])\n\n        # deterministic channel\n        channel2 = await self.channel_create('@foo2')\n\n        await self.stream_create('stream-in-channel1', '0.01', channel_id=self.get_claim_id(channel1))\n        await self.stream_create('stream-in-channel2', '0.01', channel_id=self.get_claim_id(channel2))\n\n        resolved_stream1 = await self.resolve('@foo1/stream-in-channel1')\n        self.assertEqual('stream-in-channel1', resolved_stream1['name'])\n        self.assertTrue(resolved_stream1['is_channel_signature_valid'])\n\n        resolved_stream2 = await self.resolve('@foo2/stream-in-channel2')\n        self.assertEqual('stream-in-channel2', resolved_stream2['name'])\n        self.assertTrue(resolved_stream2['is_channel_signature_valid'])\n\n    async def test_deterministic_channel_keys(self):\n        seed = self.account.seed\n        keys = self.account.deterministic_channel_keys\n\n        # create two channels and make sure they have different keys\n        channel1a = await self.channel_create('@foo1')\n        channel2a = await self.channel_create('@foo2')\n        self.assertNotEqual(\n            channel1a['outputs'][0]['value']['public_key'],\n            channel2a['outputs'][0]['value']['public_key'],\n        )\n\n        # start another daemon from the same seed\n        self.daemon2 = await self.add_daemon(seed=seed)\n        channel2b, channel1b = (await self.daemon2.jsonrpc_channel_list())['items']\n\n        # both daemons end up with the same channel signing keys automagically\n        self.assertTrue(channel1b.has_private_key)\n        self.assertEqual(\n            channel1a['outputs'][0]['value']['public_key_id'],\n            channel1b.private_key.address\n        )\n        self.assertTrue(channel2b.has_private_key)\n        self.assertEqual(\n            channel2a['outputs'][0]['value']['public_key_id'],\n            channel2b.private_key.address\n        )\n\n        # repeatedly calling next channel key returns the same key when not used\n        current_known = keys.last_known\n        next_key = await keys.generate_next_key()\n        self.assertEqual(current_known, keys.last_known)\n        self.assertEqual(next_key.address, (await keys.generate_next_key()).address)\n        # again, should be idempotent\n        next_key = await keys.generate_next_key()\n        self.assertEqual(current_known, keys.last_known)\n        self.assertEqual(next_key.address, (await keys.generate_next_key()).address)\n\n        # create third channel while both daemons running, second daemon should pick it up\n        channel3a = await self.channel_create('@foo3')\n        self.assertEqual(current_known+1, keys.last_known)\n        self.assertNotEqual(next_key.address, (await keys.generate_next_key()).address)\n        channel3b, = (await self.daemon2.jsonrpc_channel_list(name='@foo3'))['items']\n        self.assertTrue(channel3b.has_private_key)\n        self.assertEqual(\n            channel3a['outputs'][0]['value']['public_key_id'],\n            channel3b.private_key.address\n        )\n\n        # channel key cache re-populated after simulated restart\n\n        # reset cache\n        self.account.deterministic_channel_keys = DeterministicChannelKeyManager(self.account)\n        channel3c, channel2c, channel1c = (await self.daemon.jsonrpc_channel_list())['items']\n        self.assertFalse(channel1c.has_private_key)\n        self.assertFalse(channel2c.has_private_key)\n        self.assertFalse(channel3c.has_private_key)\n\n        # repopulate cache\n        await self.account.deterministic_channel_keys.ensure_cache_primed()\n        self.assertEqual(self.account.deterministic_channel_keys.last_known, keys.last_known)\n        channel3c, channel2c, channel1c = (await self.daemon.jsonrpc_channel_list())['items']\n        self.assertTrue(channel1c.has_private_key)\n        self.assertTrue(channel2c.has_private_key)\n        self.assertTrue(channel3c.has_private_key)\n\n    async def test_time_locked_transactions(self):\n        address = await self.account.receiving.get_or_create_usable_address()\n        private_key = await self.ledger.get_private_key_for_address(self.wallet, address)\n\n        script = InputScript(\n            template=InputScript.TIME_LOCK_SCRIPT,\n            values={'height': 210, 'pubkey_hash': self.ledger.address_to_hash160(address)}\n        )\n        script_address = self.ledger.hash160_to_script_address(hash160(script.source))\n        script_source = hexlify(script.source).decode()\n\n        await self.assertBalance(self.account, '10.0')\n        tx = await self.daemon.jsonrpc_account_send('4.0', script_address)\n        await self.confirm_tx(tx.id)\n        await self.generate(510)\n        await self.assertBalance(self.account, '5.999877')\n        tx = await self.daemon.jsonrpc_account_deposit(\n            tx.id, 0, script_source,\n            Base58.encode_check(self.ledger.private_key_to_wif(private_key.private_key_bytes))\n        )\n        await self.confirm_tx(tx.id)\n        await self.assertBalance(self.account, '9.9997545')\n"
  },
  {
    "path": "tests/integration/blockchain/test_blockchain_reorganization.py",
    "content": "import logging\nimport asyncio\nfrom binascii import hexlify\nfrom lbry.testcase import CommandTestCase\n\n\nclass BlockchainReorganizationTests(CommandTestCase):\n\n    VERBOSITY = logging.WARN\n\n    async def assertBlockHash(self, height):\n        bp = self.conductor.spv_node.writer\n        reader = self.conductor.spv_node.server\n\n        def get_txids():\n            return [\n                reader.db.fs_tx_hash(tx_num)[0][::-1].hex()\n                for tx_num in range(bp.db.tx_counts[height - 1], bp.db.tx_counts[height])\n            ]\n\n        block_hash = await self.blockchain.get_block_hash(height)\n\n        self.assertEqual(block_hash, (await self.ledger.headers.hash(height)).decode())\n        self.assertEqual(block_hash, (await reader.db.fs_block_hashes(height, 1))[0][::-1].hex())\n\n        txids = await asyncio.get_event_loop().run_in_executor(None, get_txids)\n        txs = await reader.db.get_transactions_and_merkles(txids)\n        block_txs = (await bp.daemon.deserialised_block(block_hash))['tx']\n        self.assertSetEqual(set(block_txs), set(txs.keys()), msg='leveldb/lbrycrd is missing transactions')\n        self.assertListEqual(block_txs, list(txs.keys()), msg='leveldb/lbrycrd transactions are of order')\n\n    async def test_reorg(self):\n        bp = self.conductor.spv_node.writer\n        bp.reorg_count_metric.set(0)\n        # invalidate current block, move forward 2\n        height = 206\n        self.assertEqual(self.ledger.headers.height, height)\n        await self.assertBlockHash(height)\n        block_hash = (await self.ledger.headers.hash(206)).decode()\n        await self.blockchain.invalidate_block(block_hash)\n        await self.blockchain.generate(2)\n        await asyncio.wait_for(self.on_header(207), 3.0)\n        self.assertEqual(self.ledger.headers.height, 207)\n        await self.assertBlockHash(206)\n        await self.assertBlockHash(207)\n        self.assertEqual(1, bp.reorg_count_metric._samples()[0][2])\n\n        # invalidate current block, move forward 3\n        await self.blockchain.invalidate_block((await self.ledger.headers.hash(206)).decode())\n        await self.blockchain.generate(3)\n        await asyncio.wait_for(self.on_header(208), 3.0)\n        self.assertEqual(self.ledger.headers.height, 208)\n        await self.assertBlockHash(206)\n        await self.assertBlockHash(207)\n        await self.assertBlockHash(208)\n        self.assertEqual(2, bp.reorg_count_metric._samples()[0][2])\n        await self.blockchain.generate(3)\n        await asyncio.wait_for(self.on_header(211), 3.0)\n        await self.assertBlockHash(209)\n        await self.assertBlockHash(210)\n        await self.assertBlockHash(211)\n        still_valid = await self.daemon.jsonrpc_stream_create(\n            'still-valid', '1.0', file_path=self.create_upload_file(data=b'hi!')\n        )\n        await self.ledger.wait(still_valid)\n        await self.blockchain.generate(1)\n        await asyncio.wait_for(self.on_header(212), 1.0)\n        claim_id = still_valid.outputs[0].claim_id\n        c1 = (await self.resolve(f'still-valid#{claim_id}'))['claim_id']\n        c2 = (await self.resolve(f'still-valid#{claim_id[:2]}'))['claim_id']\n        c3 = (await self.resolve(f'still-valid'))['claim_id']\n        self.assertTrue(c1 == c2 == c3)\n\n        abandon_tx = await self.daemon.jsonrpc_stream_abandon(claim_id=claim_id)\n        await self.blockchain.generate(1)\n        await asyncio.wait_for(self.on_header(213), 1.0)\n        c1 = await self.resolve(f'still-valid#{still_valid.outputs[0].claim_id}')\n        c2 = await self.daemon.jsonrpc_resolve([f'still-valid#{claim_id[:2]}'])\n        c3 = await self.daemon.jsonrpc_resolve([f'still-valid'])\n\n    async def test_reorg_change_claim_height(self):\n        # sanity check\n        result = await self.resolve('hovercraft')  # TODO: do these for claim_search and resolve both\n        self.assertIn('error', result)\n\n        still_valid = await self.daemon.jsonrpc_stream_create(\n            'still-valid', '1.0', file_path=self.create_upload_file(data=b'hi!')\n        )\n        await self.ledger.wait(still_valid)\n        await self.generate(1)\n\n        # create a claim and verify it's returned by claim_search\n        self.assertEqual(self.ledger.headers.height, 207)\n        await self.assertBlockHash(207)\n\n        broadcast_tx = await self.daemon.jsonrpc_stream_create(\n            'hovercraft', '1.0', file_path=self.create_upload_file(data=b'hi!')\n        )\n        await self.ledger.wait(broadcast_tx)\n        await self.generate(1)\n        await self.ledger.wait(broadcast_tx, self.blockchain.block_expected)\n        self.assertEqual(self.ledger.headers.height, 208)\n        await self.assertBlockHash(208)\n\n        claim = await self.resolve('hovercraft')\n        self.assertEqual(claim['txid'], broadcast_tx.id)\n        self.assertEqual(claim['height'], 208)\n\n        # check that our tx is in block 208 as returned by lbrycrdd\n        invalidated_block_hash = (await self.ledger.headers.hash(208)).decode()\n        block_207 = await self.blockchain.get_block(invalidated_block_hash)\n        self.assertIn(claim['txid'], block_207['tx'])\n        self.assertEqual(208, claim['height'])\n\n        # reorg the last block dropping our claim tx\n        await self.blockchain.invalidate_block(invalidated_block_hash)\n        await self.conductor.clear_mempool()\n        await self.blockchain.generate(2)\n        await asyncio.wait_for(self.on_header(209), 3.0)\n\n        await self.assertBlockHash(207)\n        await self.assertBlockHash(208)\n        await self.assertBlockHash(209)\n\n        # verify the claim was dropped from block 208 as returned by lbrycrdd\n        reorg_block_hash = await self.blockchain.get_block_hash(208)\n        self.assertNotEqual(invalidated_block_hash, reorg_block_hash)\n        block_207 = await self.blockchain.get_block(reorg_block_hash)\n        self.assertNotIn(claim['txid'], block_207['tx'])\n\n        client_reorg_block_hash = (await self.ledger.headers.hash(208)).decode()\n        self.assertEqual(client_reorg_block_hash, reorg_block_hash)\n\n        # verify the dropped claim is no longer returned by claim search\n        self.assertDictEqual(\n            {'error': {'name': 'NOT_FOUND', 'text': 'Could not find claim at \"hovercraft\".'}},\n            await self.resolve('hovercraft')\n        )\n\n        # verify the claim published a block earlier wasn't also reverted\n        self.assertEqual(207, (await self.resolve('still-valid'))['height'])\n\n        # broadcast the claim in a different block\n        new_txid = await self.blockchain.sendrawtransaction(hexlify(broadcast_tx.raw).decode())\n        self.assertEqual(broadcast_tx.id, new_txid)\n\n        await self.blockchain.generate(1)\n        await asyncio.wait_for(self.on_header(210), 1.0)\n\n        # verify the claim is in the new block and that it is returned by claim_search\n        republished = await self.resolve('hovercraft')\n        self.assertEqual(210, republished['height'])\n        self.assertEqual(claim['claim_id'], republished['claim_id'])\n\n        # this should still be unchanged\n        self.assertEqual(207, (await self.resolve('still-valid'))['height'])\n\n    async def test_reorg_drop_claim(self):\n        # sanity check\n        result = await self.resolve('hovercraft')  # TODO: do these for claim_search and resolve both\n        self.assertIn('error', result)\n\n        still_valid = await self.daemon.jsonrpc_stream_create(\n            'still-valid', '1.0', file_path=self.create_upload_file(data=b'hi!')\n        )\n        await self.ledger.wait(still_valid)\n        await self.generate(1)\n\n        # create a claim and verify it's returned by claim_search\n        self.assertEqual(self.ledger.headers.height, 207)\n        await self.assertBlockHash(207)\n\n        broadcast_tx = await self.daemon.jsonrpc_stream_create(\n            'hovercraft', '1.0', file_path=self.create_upload_file(data=b'hi!')\n        )\n        await self.ledger.wait(broadcast_tx)\n        await self.generate(1)\n        await self.ledger.wait(broadcast_tx, self.blockchain.block_expected)\n        self.assertEqual(self.ledger.headers.height, 208)\n        await self.assertBlockHash(208)\n\n        claim = await self.resolve('hovercraft')\n        self.assertEqual(claim['txid'], broadcast_tx.id)\n        self.assertEqual(claim['height'], 208)\n\n        # check that our tx is in block 208 as returned by lbrycrdd\n        invalidated_block_hash = (await self.ledger.headers.hash(208)).decode()\n        block_207 = await self.blockchain.get_block(invalidated_block_hash)\n        self.assertIn(claim['txid'], block_207['tx'])\n        self.assertEqual(208, claim['height'])\n\n        # reorg the last block dropping our claim tx\n        await self.blockchain.invalidate_block(invalidated_block_hash)\n        await self.conductor.clear_mempool()\n        await self.blockchain.generate(2)\n\n        # wait for the client to catch up and verify the reorg\n        await asyncio.wait_for(self.on_header(209), 3.0)\n        await self.assertBlockHash(207)\n        await self.assertBlockHash(208)\n        await self.assertBlockHash(209)\n\n        # verify the claim was dropped from block 208 as returned by lbrycrdd\n        reorg_block_hash = await self.blockchain.get_block_hash(208)\n        self.assertNotEqual(invalidated_block_hash, reorg_block_hash)\n        block_207 = await self.blockchain.get_block(reorg_block_hash)\n        self.assertNotIn(claim['txid'], block_207['tx'])\n\n        client_reorg_block_hash = (await self.ledger.headers.hash(208)).decode()\n        self.assertEqual(client_reorg_block_hash, reorg_block_hash)\n\n        # verify the dropped claim is no longer returned by claim search\n        self.assertDictEqual(\n            {'error': {'name': 'NOT_FOUND', 'text': 'Could not find claim at \"hovercraft\".'}},\n            await self.resolve('hovercraft')\n        )\n\n        # verify the claim published a block earlier wasn't also reverted\n        self.assertEqual(207, (await self.resolve('still-valid'))['height'])\n\n        # broadcast the claim in a different block\n        new_txid = await self.blockchain.sendrawtransaction(hexlify(broadcast_tx.raw).decode())\n        self.assertEqual(broadcast_tx.id, new_txid)\n        await self.blockchain.generate(1)\n        await asyncio.wait_for(self.on_header(210), 1.0)\n\n        # verify the claim is in the new block and that it is returned by claim_search\n        republished = await self.resolve('hovercraft')\n        self.assertEqual(210, republished['height'])\n        self.assertEqual(claim['claim_id'], republished['claim_id'])\n\n        # this should still be unchanged\n        self.assertEqual(207, (await self.resolve('still-valid'))['height'])\n"
  },
  {
    "path": "tests/integration/blockchain/test_network.py",
    "content": "import asyncio\nimport hub\n\nfrom unittest.mock import Mock\n\nfrom hub.herald import HUB_PROTOCOL_VERSION\nfrom hub.herald.udp import StatusServer\nfrom hub.herald.session import LBRYElectrumX\nfrom hub.scribe.network import LBCRegTest\n\nfrom lbry.wallet.network import Network\nfrom lbry.wallet.orchstr8 import Conductor\nfrom lbry.wallet.orchstr8.node import SPVNode\nfrom lbry.wallet.rpc import RPCSession\nfrom lbry.testcase import IntegrationTestCase, AsyncioTestCase\nfrom lbry.conf import Config\n\n\nclass NetworkTests(IntegrationTestCase):\n\n    async def test_remote_height_updated_automagically(self):\n        initial_height = self.ledger.network.remote_height\n        await self.blockchain.generate(1)\n        await self.ledger.network.on_header.first\n        self.assertEqual(self.ledger.network.remote_height, initial_height + 1)\n\n    async def test_server_features(self):\n        self.assertDictEqual({\n            'genesis_hash': LBCRegTest.GENESIS_HASH,\n            'hash_function': 'sha256',\n            'hosts': {},\n            'protocol_max': '0.199.0',\n            'protocol_min': '0.54.0',\n            'pruning': None,\n            'description': '',\n            'payment_address': '',\n            'donation_address': '',\n            'daily_fee': '0',\n            'server_version': HUB_PROTOCOL_VERSION,\n            'trending_algorithm': 'fast_ar',\n            }, await self.ledger.network.get_server_features())\n        # await self.conductor.spv_node.stop()\n        payment_address, donation_address = await self.account.get_addresses(limit=2)\n\n        original_address = self.conductor.spv_node.server.env.payment_address\n        original_donation_address = self.conductor.spv_node.server.env.donation_address\n        original_description = self.conductor.spv_node.server.env.description\n        original_daily_fee = self.conductor.spv_node.server.env.daily_fee\n\n        self.conductor.spv_node.server.env.payment_address = payment_address\n        self.conductor.spv_node.server.env.donation_address = donation_address\n        self.conductor.spv_node.server.env.description = 'Fastest server in the west.'\n        self.conductor.spv_node.server.env.daily_fee = '42'\n\n        LBRYElectrumX.set_server_features(self.conductor.spv_node.server.env)\n\n        # await self.ledger.network.on_connected.first\n        self.assertDictEqual({\n            'genesis_hash': LBCRegTest.GENESIS_HASH,\n            'hash_function': 'sha256',\n            'hosts': {},\n            'protocol_max': '0.199.0',\n            'protocol_min': '0.54.0',\n            'pruning': None,\n            'description': 'Fastest server in the west.',\n            'payment_address': payment_address,\n            'donation_address': donation_address,\n            'daily_fee': '42',\n            'server_version': HUB_PROTOCOL_VERSION,\n            'trending_algorithm': 'fast_ar',\n            }, await self.ledger.network.get_server_features())\n\n        # cleanup the changes since the attributes are set on the class\n        self.conductor.spv_node.server.env.payment_address = original_address\n        self.conductor.spv_node.server.env.donation_address = original_donation_address\n        self.conductor.spv_node.server.env.description = original_description\n        self.conductor.spv_node.server.env.daily_fee = original_daily_fee\n        LBRYElectrumX.set_server_features(self.conductor.spv_node.server.env)\n\n\nclass ReconnectTests(IntegrationTestCase):\n\n    async def test_multiple_servers(self):\n        # we have a secondary node that connects later, so\n        node2 = SPVNode(node_number=2)\n        await node2.start(self.blockchain)\n\n        self.ledger.network.config['explicit_servers'].append((node2.hostname, node2.port))\n        self.ledger.network.config['explicit_servers'].reverse()\n        self.assertEqual(50002, self.ledger.network.client.server[1])\n        await self.ledger.stop()\n        await self.ledger.start()\n\n        self.assertTrue(self.ledger.network.is_connected)\n        self.assertEqual(50003, self.ledger.network.client.server[1])\n        await node2.stop(True)\n        self.assertFalse(self.ledger.network.is_connected)\n        await self.ledger.resolve([], ['derp'])\n        self.assertEqual(50002, self.ledger.network.client.server[1])\n\n    async def test_direct_sync(self):\n        await self.ledger.stop()\n        initial_height = self.ledger.local_height_including_downloaded_height\n        await self.blockchain.generate(100)\n        while self.conductor.spv_node.server.session_manager.notified_height < initial_height + 100:\n            await asyncio.sleep(0.1)\n        self.assertEqual(initial_height, self.ledger.local_height_including_downloaded_height)\n        await self.ledger.headers.open()\n        await self.ledger.network.start()\n        await self.ledger.network.on_connected.first\n        await self.ledger.initial_headers_sync()\n        self.assertEqual(initial_height + 100, self.ledger.local_height_including_downloaded_height)\n\n    async def test_connection_drop_still_receives_events_after_reconnected(self):\n        address1 = await self.account.receiving.get_or_create_usable_address()\n        # disconnect and send a new tx, should reconnect and get it\n        self.ledger.network.client.transport.close()\n        self.assertFalse(self.ledger.network.is_connected)\n        await self.ledger.resolve([], ['derp'])\n        sendtxid = await self.send_to_address_and_wait(address1, 1.1337, 1)\n        self.assertLess(self.ledger.network.client.response_time, 1)  # response time properly set lower, we are fine\n\n        await self.assertBalance(self.account, '1.1337')\n        # is it real? are we rich!? let me see this tx...\n        d = self.ledger.network.get_transaction(sendtxid)\n        # what's that smoke on my ethernet cable? oh no!\n        master_client = self.ledger.network.client\n        self.ledger.network.client.connection_lost(Exception())\n        with self.assertRaises(asyncio.TimeoutError):\n            await d\n        self.assertIsNone(master_client.response_time)  # response time unknown as it failed\n        # rich but offline? no way, no water, let's retry\n        with self.assertRaisesRegex(ConnectionError, 'connection is not available'):\n            await self.ledger.network.get_transaction(sendtxid)\n        # * goes to pick some water outside... * time passes by and another donation comes in\n        sendtxid = await self.blockchain.send_to_address(address1, 42)\n        await self.blockchain.generate(1)\n        # (this is just so the test doesn't hang forever if it doesn't reconnect)\n        if not self.ledger.network.is_connected:\n            await asyncio.wait_for(self.ledger.network.on_connected.first, timeout=10.0)\n        # omg, the burned cable still works! torba is fire proof!\n        await self.ledger.on_header.where(self.blockchain.is_expected_block)\n        await self.ledger.network.get_transaction(sendtxid)\n\n    async def test_timeout_then_reconnect(self):\n        # tests that it connects back after some failed attempts\n        await self.conductor.spv_node.stop()\n        self.assertFalse(self.ledger.network.is_connected)\n        await asyncio.sleep(0.2)  # let it retry and fail once\n        await self.conductor.spv_node.start(self.conductor.lbcwallet_node)\n        await self.ledger.network.on_connected.first\n        self.assertTrue(self.ledger.network.is_connected)\n\n    async def test_timeout_and_concurrency_propagated_from_config(self):\n        conf = Config()\n        self.assertEqual(self.ledger.network.client.timeout, 30)\n        self.assertEqual(self.ledger.network.client.concurrency, 32)\n        conf.hub_timeout = 123.0\n        conf.concurrent_hub_requests = 42\n        conf.known_hubs = self.ledger.config['known_hubs']\n        conf.wallet_dir = self.ledger.config['data_path']\n        self.manager.config = conf\n        await self.manager.reset()\n        self.assertEqual(self.ledger.network.client.timeout, 123)\n        self.assertEqual(self.ledger.network.client.concurrency, 42)\n\n    # async def test_online_but_still_unavailable(self):\n    #     # Edge case. See issue #2445 for context\n    #     self.assertIsNotNone(self.ledger.network.session_pool.fastest_session)\n    #     for session in self.ledger.network.session_pool.sessions:\n    #         session.response_time = None\n    #     self.assertIsNone(self.ledger.network.session_pool.fastest_session)\n\n\nclass UDPServerFailDiscoveryTest(AsyncioTestCase):\n    async def test_wallet_connects_despite_lack_of_udp(self):\n        conductor = Conductor()\n        conductor.spv_node.udp_port = '0'\n        await conductor.start_lbcd()\n        self.addCleanup(conductor.stop_lbcd)\n        await conductor.start_lbcwallet()\n        self.addCleanup(conductor.stop_lbcwallet)\n        await conductor.start_spv()\n        self.addCleanup(conductor.stop_spv)\n        self.assertFalse(conductor.spv_node.server.status_server.is_running)\n        await asyncio.wait_for(conductor.start_wallet(), timeout=5)\n        self.addCleanup(conductor.stop_wallet)\n        self.assertTrue(conductor.wallet_node.ledger.network.is_connected)\n\n\nclass ServerPickingTestCase(AsyncioTestCase):\n    async def _make_udp_server(self, port, latency) -> StatusServer:\n        s = StatusServer()\n        await s.start(0, b'\\x00' * 32, 'US', '127.0.0.1', port, True)\n        s.set_available()\n        sendto = s._protocol.transport.sendto\n\n        def mock_sendto(data, addr):\n            self.loop.call_later(latency, sendto, data, addr)\n\n        s._protocol.transport.sendto = mock_sendto\n\n        self.addCleanup(s.stop)\n        return s\n\n    async def _make_fake_server(self, latency=1.0, port=1):\n        # local fake server with artificial latency\n        class FakeSession(RPCSession):\n            async def handle_request(self, request):\n                await asyncio.sleep(latency)\n                if request.method == 'server.version':\n                    return tuple(request.args)\n                return {'height': 1}\n        server = await self.loop.create_server(lambda: FakeSession(), host='127.0.0.1', port=port)\n        self.addCleanup(server.close)\n        await self._make_udp_server(port, latency)\n        return '127.0.0.1', port\n\n    async def _make_bad_server(self, port=42420):\n        async def echo(reader, writer):\n            while True:\n                writer.write(await reader.read())\n\n        server = await asyncio.start_server(echo, host='127.0.0.1', port=port)\n        self.addCleanup(server.close)\n        await self._make_udp_server(port, 0)\n        return '127.0.0.1', port\n\n    async def test_pick_fastest(self):\n        ledger = Mock(config={\n            'default_servers': [\n                # fast but unhealthy, should be discarded\n                # await self._make_bad_server(),\n                ('localhost', 1),\n                ('example.that.doesnt.resolve', 9000),\n                await self._make_fake_server(latency=1.0, port=1340),\n                await self._make_fake_server(latency=0.1, port=1337),\n                await self._make_fake_server(latency=0.4, port=1339),\n            ],\n            'connect_timeout': 3\n        })\n\n        network = Network(ledger)\n        self.addCleanup(network.stop)\n        await network.start()\n        await asyncio.wait_for(network.on_connected.first, timeout=10)\n        self.assertTrue(network.is_connected)\n        self.assertTupleEqual(network.client.server, ('127.0.0.1', 1337))\n        # self.assertTrue(all([not session.is_closing() for session in network.session_pool.available_sessions]))\n        # ensure we are connected to all of them after a while\n        # await asyncio.sleep(1)\n        # self.assertEqual(len(list(network.session_pool.available_sessions)), 3)\n"
  },
  {
    "path": "tests/integration/blockchain/test_purchase_command.py",
    "content": "from typing import Optional\nfrom lbry.testcase import CommandTestCase\nfrom lbry.schema.purchase import Purchase\nfrom lbry.wallet.transaction import Transaction\nfrom lbry.wallet.dewies import lbc_to_dewies, dewies_to_lbc\n\n\nclass PurchaseCommandTests(CommandTestCase):\n\n    async def asyncSetUp(self):\n        await super().asyncSetUp()\n        self.merchant_address = await self.blockchain.get_raw_change_address()\n\n    async def priced_stream(\n        self, name='stream', price: Optional[str] = '0.2', currency='LBC', mine=False\n    ) -> Transaction:\n        kwargs = {}\n        if price and currency:\n            kwargs = {\n                'fee_amount': price,\n                'fee_currency': currency,\n                'fee_address': self.merchant_address\n            }\n        if not mine:\n            kwargs['claim_address'] = self.merchant_address\n        file_path = self.create_upload_file(data=b'high value content')\n        tx = await self.daemon.jsonrpc_stream_create(\n            name, '0.01', file_path=file_path, **kwargs\n        )\n        await self.ledger.wait(tx)\n        await self.generate(1)\n        await self.ledger.wait(tx)\n        await self.daemon.jsonrpc_file_delete(claim_name=name)\n        return tx\n\n    async def create_purchase(self, name, price):\n        stream = await self.priced_stream(name, price)\n        claim_id = stream.outputs[0].claim_id\n        purchase = await self.daemon.jsonrpc_purchase_create(claim_id)\n        await self.ledger.wait(purchase)\n        return claim_id\n\n    async def assertStreamPurchased(self, stream: Transaction, operation):\n\n        await self.account.release_all_outputs()\n        buyer_balance = await self.account.get_balance()\n        merchant_balance = lbc_to_dewies(await self.blockchain.get_balance())\n        pre_purchase_count = (await self.daemon.jsonrpc_purchase_list())['total_items']\n        purchase = await operation()\n        stream_txo, purchase_txo = stream.outputs[0], purchase.outputs[0]\n        stream_fee = stream_txo.claim.stream.fee\n        self.assertEqual(stream_fee.dewies, purchase_txo.amount)\n        self.assertEqual(stream_fee.address, purchase_txo.get_address(self.ledger))\n\n        await self.ledger.wait(purchase)\n        await self.generate(1)\n        merchant_balance += lbc_to_dewies('1.0')  # block reward\n        await self.ledger.wait(purchase)\n\n        self.assertEqual(\n            await self.account.get_balance(), buyer_balance - (purchase.input_sum-purchase.outputs[2].amount))\n        self.assertEqual(\n            str(float(await self.blockchain.get_balance())),\n            dewies_to_lbc(merchant_balance + purchase_txo.amount)\n        )\n\n        purchases = await self.daemon.jsonrpc_purchase_list()\n        self.assertEqual(purchases['total_items'], pre_purchase_count+1)\n\n        tx = purchases['items'][0].tx_ref.tx\n        self.assertEqual(len(tx.outputs), 3)  # purchase txo, purchase data, change\n\n        txo0 = tx.outputs[0]\n        txo1 = tx.outputs[1]\n        self.assertEqual(txo0.purchase, txo1)  # purchase txo has reference to purchase data\n        self.assertTrue(txo1.is_purchase_data)\n        self.assertTrue(txo1.can_decode_purchase_data)\n        self.assertIsInstance(txo1.purchase_data, Purchase)\n        self.assertEqual(txo1.purchase_data.claim_id, stream_txo.claim_id)\n\n    async def test_purchasing(self):\n        stream = await self.priced_stream()\n        claim_id = stream.outputs[0].claim_id\n\n        # explicit purchase of claim\n        await self.assertStreamPurchased(stream, lambda: self.daemon.jsonrpc_purchase_create(claim_id))\n\n        # check that `get` doesn't purchase it again\n        balance = await self.account.get_balance()\n        response = await self.daemon.jsonrpc_get('lbry://stream')\n        self.assertIsNone(response.content_fee)\n        self.assertEqual(await self.account.get_balance(), balance)\n        self.assertItemCount(await self.daemon.jsonrpc_purchase_list(), 1)\n\n        # `get` does purchase a stream we don't have yet\n        another_stream = await self.priced_stream('another')\n\n        async def imagine_its_a_lambda():\n            response = await self.daemon.jsonrpc_get('lbry://another')\n            return response.content_fee\n\n        await self.assertStreamPurchased(another_stream, imagine_its_a_lambda)\n\n        # purchase non-existent claim fails\n        with self.assertRaisesRegex(Exception, \"Could not find claim with claim_id\"):\n            await self.daemon.jsonrpc_purchase_create('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')\n\n        # purchase stream with no price fails\n        no_price_stream = await self.priced_stream('no_price_stream', price=None)\n        with self.assertRaisesRegex(Exception, \"does not have a purchase price\"):\n            await self.daemon.jsonrpc_purchase_create(no_price_stream.outputs[0].claim_id)\n\n        # purchase claim you already own fails\n        with self.assertRaisesRegex(Exception, \"You already have a purchase for claim_id\"):\n            await self.daemon.jsonrpc_purchase_create(claim_id)\n\n        # force purchasing claim you already own\n        await self.assertStreamPurchased(\n            stream, lambda: self.daemon.jsonrpc_purchase_create(claim_id, allow_duplicate_purchase=True)\n        )\n\n        # purchase by uri\n        abc_stream = await self.priced_stream('abc')\n        await self.assertStreamPurchased(abc_stream, lambda: self.daemon.jsonrpc_purchase_create(url='lbry://abc'))\n\n        # purchase without valid exchange rate fails\n        erm = self.daemon.component_manager.get_component('exchange_rate_manager')\n        for feed in erm.market_feeds:\n            feed.last_check -= 10_000\n        with self.assertRaisesRegex(Exception, \"Unable to convert 50 from USD to LBC\"):\n            await self.daemon.jsonrpc_purchase_create(claim_id, allow_duplicate_purchase=True)\n\n    async def test_purchase_and_transaction_list(self):\n        self.assertItemCount(await self.daemon.jsonrpc_purchase_list(), 0)\n        self.assertItemCount(await self.daemon.jsonrpc_transaction_list(), 1)\n\n        claim_id1 = await self.create_purchase('a', '1.0')\n        claim_id2 = await self.create_purchase('b', '1.0')\n\n        result = await self.out(self.daemon.jsonrpc_purchase_list())\n        self.assertItemCount(await self.daemon.jsonrpc_transaction_list(), 5)\n        self.assertItemCount(result, 2)\n        self.assertEqual(result['items'][0]['type'], 'purchase')\n        self.assertEqual(result['items'][0]['claim_id'], claim_id2)\n        self.assertNotIn('claim', result['items'][0])\n        self.assertEqual(result['items'][1]['type'], 'purchase')\n        self.assertEqual(result['items'][1]['claim_id'], claim_id1)\n        self.assertNotIn('claim', result['items'][1])\n\n        result = await self.out(self.daemon.jsonrpc_purchase_list(resolve=True))\n        self.assertEqual(result['items'][0]['claim']['name'], 'b')\n        self.assertEqual(result['items'][1]['claim']['name'], 'a')\n\n        result = await self.daemon.jsonrpc_transaction_list()\n        self.assertEqual(result['items'][0]['purchase_info'][0]['claim_id'], claim_id2)\n        self.assertEqual(result['items'][2]['purchase_info'][0]['claim_id'], claim_id1)\n\n        result = await self.claim_search(include_purchase_receipt=True)\n        self.assertEqual(result[0]['claim_id'], result[0]['purchase_receipt']['claim_id'])\n        self.assertEqual(result[1]['claim_id'], result[1]['purchase_receipt']['claim_id'])\n\n        url = result[0]['canonical_url']\n        resolve = await self.resolve(url, include_purchase_receipt=True)\n        self.assertEqual(result[0]['claim_id'], resolve['purchase_receipt']['claim_id'])\n\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 0)\n        await self.daemon.jsonrpc_get('lbry://a')\n        await self.daemon.jsonrpc_get('lbry://b')\n        files = await self.file_list()\n        self.assertEqual(files[0]['claim_id'], files[0]['purchase_receipt']['claim_id'])\n        self.assertEqual(files[1]['claim_id'], files[1]['purchase_receipt']['claim_id'])\n\n    async def test_seller_can_spend_received_purchase_funds(self):\n        self.merchant_address = await self.account.receiving.get_or_create_usable_address()\n        daemon2 = await self.add_daemon()\n        address2 = await daemon2.wallet_manager.default_account.receiving.get_or_create_usable_address()\n        await self.send_to_address_and_wait(address2, 2, 1, ledger=daemon2.ledger)\n\n        stream = await self.priced_stream('a', '1.0')\n        await self.assertBalance(self.account, '9.987893')\n        self.assertItemCount(await self.daemon.jsonrpc_utxo_list(), 1)\n\n        purchase = await daemon2.jsonrpc_purchase_create(stream.outputs[0].claim_id)\n        await self.ledger.wait(purchase)\n        await self.generate(1)\n        await self.ledger.wait(purchase)\n\n        # confirm that available and reserved take into account purchase received\n        self.assertEqual(await self.account.get_detailed_balance(), {\n            'total': 1099789300,\n            'available': 1098789300,\n            'reserved': 1000000,\n            'reserved_subtotals': {'claims': 1000000, 'supports': 0, 'tips': 0}\n        })\n        self.assertItemCount(await self.daemon.jsonrpc_utxo_list(), 2)\n\n        spend = await self.daemon.jsonrpc_wallet_send('10.5', address2)\n        await self.ledger.wait(spend)\n        await self.generate(1)\n        await self.ledger.wait(spend)\n        await self.assertBalance(self.account, '0.487695')\n        self.assertItemCount(await self.daemon.jsonrpc_utxo_list(), 1)\n\n    async def test_owner_not_required_purchase_own_content(self):\n        await self.priced_stream(mine=True)\n        # check that `get` doesn't purchase own claim\n        balance = await self.account.get_balance()\n        response = await self.daemon.jsonrpc_get('lbry://stream')\n        self.assertIsNone(response.content_fee)\n        self.assertEqual(await self.account.get_balance(), balance)\n        self.assertItemCount(await self.daemon.jsonrpc_purchase_list(), 0)\n"
  },
  {
    "path": "tests/integration/blockchain/test_sync.py",
    "content": "import asyncio\nimport logging\nfrom lbry.testcase import IntegrationTestCase, WalletNode\nfrom lbry.constants import CENT\nfrom lbry.wallet import WalletManager, RegTestLedger, Transaction, Output\n\n\nclass SyncTests(IntegrationTestCase):\n\n    VERBOSITY = logging.WARN\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.api_port = 5280\n        self.started_nodes = []\n\n    async def asyncTearDown(self):\n        for node in self.started_nodes:\n            try:\n                await node.stop(cleanup=True)\n            except Exception as e:\n                print(e)\n        await super().asyncTearDown()\n\n    async def make_wallet_node(self, seed=None):\n        self.api_port += 1\n        wallet_node = WalletNode(WalletManager, RegTestLedger, port=self.api_port)\n        await wallet_node.start(self.conductor.spv_node, seed)\n        self.started_nodes.append(wallet_node)\n        return wallet_node\n\n    async def test_nodes_with_same_account_stay_in_sync(self):\n        # destination node/account for receiving TXs\n        node0 = await self.make_wallet_node()\n        account0 = node0.account\n        # main node/account creating TXs\n        node1 = self.wallet_node\n        account1 = self.wallet_node.account\n        # mirror node/account, expected to reflect everything in main node as it happens\n        node2 = await self.make_wallet_node(account1.seed)\n        account2 = node2.account\n\n        self.assertNotEqual(account0.id, account1.id)\n        self.assertEqual(account1.id, account2.id)\n        await self.assertBalance(account0, '0.0')\n        await self.assertBalance(account1, '0.0')\n        await self.assertBalance(account2, '0.0')\n        self.assertEqual(await account0.get_address_count(chain=0), 20)\n        self.assertEqual(await account1.get_address_count(chain=0), 20)\n        self.assertEqual(await account2.get_address_count(chain=0), 20)\n        self.assertEqual(await account1.get_address_count(chain=1), 6)\n        self.assertEqual(await account2.get_address_count(chain=1), 6)\n\n        # check that main node and mirror node generate 5 address to fill gap\n        fifth_address = (await account1.receiving.get_addresses())[4]\n        await self.blockchain.send_to_address(fifth_address, 1.00)\n        await asyncio.wait([\n            account1.ledger.on_address.first,\n            account2.ledger.on_address.first\n        ])\n        self.assertEqual(await account1.get_address_count(chain=0), 25)\n        self.assertEqual(await account2.get_address_count(chain=0), 25)\n        await self.assertBalance(account1, '1.0')\n        await self.assertBalance(account2, '1.0')\n\n        await self.generate(1)\n\n        # pay 0.01 from main node to receiving node, would have increased change addresses\n        address0 = (await account0.receiving.get_addresses())[0]\n        hash0 = self.ledger.address_to_hash160(address0)\n        tx = await Transaction.create(\n            [],\n            [Output.pay_pubkey_hash(CENT, hash0)],\n            [account1], account1\n        )\n        await self.broadcast(tx)\n        await asyncio.wait([\n            account0.ledger.wait(tx),\n            account1.ledger.wait(tx),\n            account2.ledger.wait(tx),\n        ])\n        await self.generate(1)\n        await asyncio.wait([\n            account0.ledger.wait(tx),\n            account1.ledger.wait(tx),\n            account2.ledger.wait(tx),\n        ])\n        self.assertEqual(await account0.get_address_count(chain=0), 21)\n        self.assertGreater(await account1.get_address_count(chain=1), 6)\n        self.assertGreater(await account2.get_address_count(chain=1), 6)\n        await self.assertBalance(account0, '0.01')\n        await self.assertBalance(account1, '0.989876')\n        await self.assertBalance(account2, '0.989876')\n\n        await self.generate(1)\n\n        # create a new mirror node and see if it syncs to same balance from scratch\n        node3 = await self.make_wallet_node(account1.seed)\n        account3 = node3.account\n        await self.assertBalance(account3, '0.989876')\n"
  },
  {
    "path": "tests/integration/blockchain/test_wallet_commands.py",
    "content": "import asyncio\nimport json\nimport string\nfrom binascii import unhexlify\nfrom random import Random\n\nfrom lbry.wallet import ENCRYPT_ON_DISK\nfrom lbry.error import InvalidPasswordError\nfrom lbry.testcase import CommandTestCase\nfrom lbry.wallet.dewies import dict_values_to_lbc\n\n\nclass WalletCommands(CommandTestCase):\n\n    async def test_wallet_create_and_add_subscribe(self):\n        session = next(iter(self.conductor.spv_node.server.session_manager.sessions.values()))\n        self.assertEqual(len(session.hashX_subs), 27)\n        wallet = await self.daemon.jsonrpc_wallet_create('foo', create_account=True, single_key=True)\n        self.assertEqual(len(session.hashX_subs), 28)\n        await self.daemon.jsonrpc_wallet_remove(wallet.id)\n        self.assertEqual(len(session.hashX_subs), 27)\n        await self.daemon.jsonrpc_wallet_add(wallet.id)\n        self.assertEqual(len(session.hashX_subs), 28)\n\n    async def test_wallet_syncing_status(self):\n        address = await self.daemon.jsonrpc_address_unused()\n        await self.ledger._update_tasks.done.wait()\n        self.assertFalse(self.daemon.jsonrpc_wallet_status()['is_syncing'])\n        await self.send_to_address_and_wait(address, 1)\n        await self.ledger._update_tasks.started.wait()\n        self.assertTrue(self.daemon.jsonrpc_wallet_status()['is_syncing'])\n        await self.ledger._update_tasks.done.wait()\n        self.assertFalse(self.daemon.jsonrpc_wallet_status()['is_syncing'])\n\n        wallet = self.daemon.component_manager.get_actual_component('wallet')\n        wallet_manager = wallet.wallet_manager\n        # when component manager hasn't started yet\n        wallet.wallet_manager = None\n        self.assertEqual(\n            {'is_encrypted': None, 'is_syncing': None, 'is_locked': None},\n            self.daemon.jsonrpc_wallet_status()\n        )\n        wallet.wallet_manager = wallet_manager\n        self.assertEqual(\n            {'is_encrypted': False, 'is_syncing': False, 'is_locked': False},\n            self.daemon.jsonrpc_wallet_status()\n        )\n\n    async def test_wallet_reconnect(self):\n        status = await self.daemon.jsonrpc_status()\n        self.assertEqual(len(status['wallet']['servers']), 1)\n        self.assertEqual(status['wallet']['servers'][0]['port'], 50002)\n        await self.conductor.spv_node.stop()\n        self.conductor.spv_node.port = 54320\n        await self.conductor.spv_node.start(self.conductor.lbcwallet_node)\n        status = await self.daemon.jsonrpc_status()\n        self.assertEqual(len(status['wallet']['servers']), 0)\n        self.daemon.jsonrpc_settings_set('lbryum_servers', ['localhost:54320'])\n        await self.daemon.jsonrpc_wallet_reconnect()\n        status = await self.daemon.jsonrpc_status()\n        self.assertEqual(len(status['wallet']['servers']), 1)\n        self.assertEqual(status['wallet']['servers'][0]['port'], 54320)\n\n    async def test_sending_to_scripthash_address(self):\n        bal = await self.blockchain.get_balance()\n        await self.assertBalance(self.account, '10.0')\n        p2sh_address1 = await self.blockchain.get_new_address(self.blockchain.P2SH_SEGWIT_ADDRESS)\n        tx = await self.account_send('2.0', p2sh_address1)\n        self.assertEqual(tx['outputs'][0]['address'], p2sh_address1)\n        self.assertEqual(await self.blockchain.get_balance(), str(float(bal)+3))  # +1 lbc for confirm block\n        await self.assertBalance(self.account, '7.999877')\n        await self.wallet_send('3.0', p2sh_address1)\n        self.assertEqual(await self.blockchain.get_balance(), str(float(bal)+7))  # +1 lbc for confirm block\n        await self.assertBalance(self.account, '4.999754')\n\n    async def test_balance_caching(self):\n        account2 = await self.daemon.jsonrpc_account_create(\"Tip-er\")\n        address2 = await self.daemon.jsonrpc_address_unused(account2.id)\n        await self.send_to_address_and_wait(address2, 10, 2)\n        await self.ledger.tasks_are_done()  # don't mess with the query count while we need it\n\n        wallet_balance = self.daemon.jsonrpc_wallet_balance\n        ledger = self.ledger\n        query_count = self.ledger.db.db.query_count\n\n        expected = {\n            'total': '20.0',\n            'available': '20.0',\n            'reserved': '0.0',\n            'reserved_subtotals': {'claims': '0.0', 'supports': '0.0', 'tips': '0.0'}\n        }\n        self.assertIsNone(ledger._balance_cache.get(self.account.id))\n\n        query_count += 2\n        balance = await wallet_balance()\n        self.assertEqual(self.ledger.db.db.query_count, query_count)\n        self.assertEqual(balance, expected)\n        self.assertEqual(dict_values_to_lbc(ledger._balance_cache.get(self.account.id))['total'], '10.0')\n        self.assertEqual(dict_values_to_lbc(ledger._balance_cache.get(account2.id))['total'], '10.0')\n\n        # calling again uses cache\n        balance = await wallet_balance()\n        self.assertEqual(self.ledger.db.db.query_count, query_count)\n        self.assertEqual(balance, expected)\n        self.assertEqual(dict_values_to_lbc(ledger._balance_cache.get(self.account.id))['total'], '10.0')\n        self.assertEqual(dict_values_to_lbc(ledger._balance_cache.get(account2.id))['total'], '10.0')\n\n        await self.stream_create()\n        await self.generate(1)\n\n        expected = {\n            'total': '19.979893',\n            'available': '18.979893',\n            'reserved': '1.0',\n            'reserved_subtotals': {'claims': '1.0', 'supports': '0.0', 'tips': '0.0'}\n        }\n        # on_transaction event reset balance cache\n        query_count = self.ledger.db.db.query_count\n        self.assertEqual(await wallet_balance(), expected)\n        query_count += 1  # only one of the accounts changed\n        self.assertEqual(dict_values_to_lbc(ledger._balance_cache.get(self.account.id))['total'], '9.979893')\n        self.assertEqual(dict_values_to_lbc(ledger._balance_cache.get(account2.id))['total'], '10.0')\n        self.assertEqual(self.ledger.db.db.query_count, query_count)\n\n    async def test_granular_balances(self):\n        account2 = await self.daemon.jsonrpc_account_create(\"Tip-er\")\n        wallet2 = await self.daemon.jsonrpc_wallet_create('foo', create_account=True)\n        account3 = wallet2.default_account\n        address3 = await self.daemon.jsonrpc_address_unused(account3.id, wallet2.id)\n        await self.send_to_address_and_wait(address3, 1, 1)\n\n        account_balance = self.daemon.jsonrpc_account_balance\n        wallet_balance = self.daemon.jsonrpc_wallet_balance\n\n        expected = {\n            'total': '10.0',\n            'available': '10.0',\n            'reserved': '0.0',\n            'reserved_subtotals': {'claims': '0.0', 'supports': '0.0', 'tips': '0.0'}\n        }\n        self.assertEqual(await account_balance(), expected)\n        self.assertEqual(await wallet_balance(), expected)\n\n        # claim with update + supporting our own claim\n        stream1 = await self.stream_create('granularity', '3.0')\n        await self.stream_update(self.get_claim_id(stream1), data=b'news', bid='1.0')\n        await self.support_create(self.get_claim_id(stream1), '2.0')\n        expected = {\n            'total': '9.977534',\n            'available': '6.977534',\n            'reserved': '3.0',\n            'reserved_subtotals': {'claims': '1.0', 'supports': '2.0', 'tips': '0.0'}\n        }\n        self.assertEqual(await account_balance(), expected)\n        self.assertEqual(await wallet_balance(), expected)\n\n        address2 = await self.daemon.jsonrpc_address_unused(account2.id)\n\n        # send lbc to someone else\n        tx = await self.daemon.jsonrpc_account_send('1.0', address2, blocking=True)\n        await self.confirm_tx(tx.id)\n        self.assertEqual(await account_balance(), {\n            'total': '8.97741',\n            'available': '5.97741',\n            'reserved': '3.0',\n            'reserved_subtotals': {'claims': '1.0', 'supports': '2.0', 'tips': '0.0'}\n        })\n        self.assertEqual(await wallet_balance(), {\n            'total': '9.97741',\n            'available': '6.97741',\n            'reserved': '3.0',\n            'reserved_subtotals': {'claims': '1.0', 'supports': '2.0', 'tips': '0.0'}\n        })\n\n        # tip received\n        support1 = await self.support_create(\n            self.get_claim_id(stream1), '0.3', tip=True, wallet_id=wallet2.id\n        )\n        self.assertEqual(await account_balance(), {\n            'total': '9.27741',\n            'available': '5.97741',\n            'reserved': '3.3',\n            'reserved_subtotals': {'claims': '1.0', 'supports': '2.0', 'tips': '0.3'}\n        })\n        self.assertEqual(await wallet_balance(), {\n            'total': '10.27741',\n            'available': '6.97741',\n            'reserved': '3.3',\n            'reserved_subtotals': {'claims': '1.0', 'supports': '2.0', 'tips': '0.3'}\n        })\n\n        # tip claimed\n        tx = await self.daemon.jsonrpc_support_abandon(txid=support1['txid'], nout=0, blocking=True)\n        await self.confirm_tx(tx.id)\n        self.assertEqual(await account_balance(), {\n            'total': '9.277303',\n            'available': '6.277303',\n            'reserved': '3.0',\n            'reserved_subtotals': {'claims': '1.0', 'supports': '2.0', 'tips': '0.0'}\n        })\n        self.assertEqual(await wallet_balance(), {\n            'total': '10.277303',\n            'available': '7.277303',\n            'reserved': '3.0',\n            'reserved_subtotals': {'claims': '1.0', 'supports': '2.0', 'tips': '0.0'}\n        })\n\n        stream2 = await self.stream_create(\n            'granularity-is-cool', '0.1', account_id=account2.id, funding_account_ids=[account2.id]\n        )\n\n        # tip another claim\n        await self.support_create(\n            self.get_claim_id(stream2), '0.2', tip=True, wallet_id=wallet2.id\n        )\n        self.assertEqual(await account_balance(), {\n            'total': '9.277303',\n            'available': '6.277303',\n            'reserved': '3.0',\n            'reserved_subtotals': {'claims': '1.0', 'supports': '2.0', 'tips': '0.0'}\n        })\n        self.assertEqual(await wallet_balance(), {\n            'total': '10.439196',\n            'available': '7.139196',\n            'reserved': '3.3',\n            'reserved_subtotals': {'claims': '1.1', 'supports': '2.0', 'tips': '0.2'}\n        })\n\n\nclass WalletEncryptionAndSynchronization(CommandTestCase):\n\n    SEED = (\n        \"carbon smart garage balance margin twelve chest \"\n        \"sword toast envelope bottom stomach absent\"\n    )\n\n    async def asyncSetUp(self):\n        await super().asyncSetUp()\n        self.daemon2 = await self.add_daemon(\n            seed=\"chest sword toast envelope bottom stomach absent \"\n                 \"carbon smart garage balance margin twelve\"\n        )\n        address = (await self.daemon2.wallet_manager.default_account.receiving.get_addresses(limit=1, only_usable=True))[0]\n        await self.send_to_address_and_wait(address, 1, 1, ledger=self.daemon2.ledger)\n\n    def assertWalletEncrypted(self, wallet_path, encrypted):\n        with open(wallet_path) as opened:\n            wallet = json.load(opened)\n            self.assertEqual(wallet['accounts'][0]['private_key'][1:4] != 'prv', encrypted)\n\n    async def test_sync(self):\n        daemon, daemon2 = self.daemon, self.daemon2\n\n        # Preferences\n        self.assertFalse(daemon.jsonrpc_preference_get())\n        self.assertFalse(daemon2.jsonrpc_preference_get())\n\n        daemon.jsonrpc_preference_set(\"fruit\", '[\"peach\", \"apricot\"]')\n        daemon.jsonrpc_preference_set(\"one\", \"1\")\n        daemon.jsonrpc_preference_set(\"conflict\", \"1\")\n        daemon2.jsonrpc_preference_set(\"another\", \"A\")\n        await asyncio.sleep(1)\n        # these preferences will win after merge since they are \"newer\"\n        daemon2.jsonrpc_preference_set(\"two\", \"2\")\n        daemon2.jsonrpc_preference_set(\"conflict\", \"2\")\n        daemon.jsonrpc_preference_set(\"another\", \"B\")\n\n        self.assertDictEqual(daemon.jsonrpc_preference_get(), {\n            \"one\": \"1\", \"conflict\": \"1\", \"another\": \"B\", \"fruit\": [\"peach\", \"apricot\"]\n        })\n        self.assertDictEqual(daemon2.jsonrpc_preference_get(), {\n            \"two\": \"2\", \"conflict\": \"2\", \"another\": \"A\"\n        })\n\n        self.assertItemCount(await daemon.jsonrpc_account_list(), 1)\n\n        data = await daemon2.jsonrpc_sync_apply('password')\n        await daemon.jsonrpc_sync_apply('password', data=data['data'], blocking=True)\n\n        self.assertItemCount(await daemon.jsonrpc_account_list(), 2)\n        self.assertDictEqual(\n            # \"two\" key added and \"conflict\" value changed to \"2\"\n            daemon.jsonrpc_preference_get(),\n            {\"one\": \"1\", \"two\": \"2\", \"conflict\": \"2\", \"another\": \"B\", \"fruit\": [\"peach\", \"apricot\"]}\n        )\n\n        # Channel Certificate\n        # non-deterministic channel\n        self.daemon2.wallet_manager.default_account.channel_keys['mqs77XbdnuxWN4cXrjKbSoGLkvAHa4f4B8'] = (\n            '-----BEGIN EC PRIVATE KEY-----\\nMHQCAQEEIBZRTZ7tHnYCH3IE9mCo95'\n            '466L/ShYFhXGrjmSMFJw8eoAcGBSuBBAAK\\noUQDQgAEmucoPz9nI+ChZrfhnh'\n            '0RZ/bcX0r2G0pYBmoNKovtKzXGa8y07D66MWsW\\nqXptakqO/9KddIkBu5eJNS'\n            'UZzQCxPQ==\\n-----END EC PRIVATE KEY-----\\n'\n        )\n        channel = await self.create_nondeterministic_channel('@foo', '0.1', unhexlify(\n            '3056301006072a8648ce3d020106052b8104000a034200049ae7283f3f6723e0a1'\n            '66b7e19e1d1167f6dc5f4af61b4a58066a0d2a8bed2b35c66bccb4ec3eba316b16'\n            'a97a6d6a4a8effd29d748901bb9789352519cd00b13d'\n        ), self.daemon2, blocking=True)\n        await self.confirm_tx(channel['txid'], self.daemon2.ledger)\n\n        # both daemons will have the channel but only one has the cert so far\n        self.assertItemCount(await daemon.jsonrpc_channel_list(), 1)\n        self.assertEqual(len(daemon.wallet_manager.default_wallet.accounts[1].channel_keys), 0)\n        self.assertItemCount(await daemon2.jsonrpc_channel_list(), 1)\n        self.assertEqual(len(daemon2.wallet_manager.default_account.channel_keys), 1)\n\n        data = await daemon2.jsonrpc_sync_apply('password')\n        await daemon.jsonrpc_sync_apply('password', data=data['data'], blocking=True)\n\n        # both daemons have the cert after sync'ing\n        self.assertEqual(\n            daemon2.wallet_manager.default_account.channel_keys,\n            daemon.wallet_manager.default_wallet.accounts[1].channel_keys\n        )\n\n    async def test_encryption_and_locking(self):\n        daemon = self.daemon\n        wallet = daemon.wallet_manager.default_wallet\n        wallet.save()\n\n        self.assertEqual(daemon.jsonrpc_wallet_status(), {\n            'is_locked': False, 'is_encrypted': False, 'is_syncing': False\n        })\n        self.assertIsNone(daemon.jsonrpc_preference_get(ENCRYPT_ON_DISK))\n        self.assertWalletEncrypted(wallet.storage.path, False)\n\n        # can't lock an unencrypted account\n        with self.assertRaisesRegex(AssertionError, \"Cannot lock an unencrypted wallet, encrypt first.\"):\n            daemon.jsonrpc_wallet_lock()\n        # safe to call unlock and decrypt, they are no-ops at this point\n        await daemon.jsonrpc_wallet_unlock('password')  # already unlocked\n        daemon.jsonrpc_wallet_decrypt()  # already not encrypted\n\n        daemon.jsonrpc_wallet_encrypt('password')\n        self.assertEqual(daemon.jsonrpc_wallet_status(), {'is_locked': False, 'is_encrypted': True,\n                                                          'is_syncing': False})\n        self.assertEqual(daemon.jsonrpc_preference_get(ENCRYPT_ON_DISK), {'encrypt-on-disk': True})\n        self.assertWalletEncrypted(wallet.storage.path, True)\n\n        daemon.jsonrpc_wallet_lock()\n        self.assertEqual(daemon.jsonrpc_wallet_status(), {'is_locked': True, 'is_encrypted': True,\n                                                          'is_syncing': False})\n\n        # can't sign transactions with locked wallet\n        with self.assertRaises(AssertionError):\n            await daemon.jsonrpc_channel_create('@foo', '1.0')\n        await daemon.jsonrpc_wallet_unlock('password')\n        self.assertEqual(daemon.jsonrpc_wallet_status(), {'is_locked': False, 'is_encrypted': True,\n                                                          'is_syncing': False})\n        await daemon.jsonrpc_channel_create('@foo', '1.0')\n\n        daemon.jsonrpc_wallet_decrypt()\n        self.assertEqual(daemon.jsonrpc_wallet_status(), {'is_locked': False, 'is_encrypted': False,\n                                                          'is_syncing': False})\n        self.assertEqual(daemon.jsonrpc_preference_get(ENCRYPT_ON_DISK), {'encrypt-on-disk': False})\n        self.assertWalletEncrypted(wallet.storage.path, False)\n\n    async def test_encryption_with_imported_channel(self):\n        daemon, daemon2 = self.daemon, self.daemon2\n        channel = await self.channel_create()\n        exported = await daemon.jsonrpc_channel_export(self.get_claim_id(channel))\n        await daemon2.jsonrpc_channel_import(exported)\n        self.assertTrue(daemon2.jsonrpc_wallet_encrypt('password'))\n        self.assertTrue(daemon2.jsonrpc_wallet_lock())\n        self.assertTrue(await daemon2.jsonrpc_wallet_unlock(\"password\"))\n        self.assertEqual(daemon2.jsonrpc_wallet_status(),\n                         {'is_locked': False, 'is_encrypted': True, 'is_syncing': False})\n\n    async def test_locking_unlocking_does_not_break_deterministic_channels(self):\n        self.assertTrue(self.daemon.jsonrpc_wallet_encrypt(\"password\"))\n        self.assertTrue(self.daemon.jsonrpc_wallet_lock())\n        self.account.deterministic_channel_keys._private_key = None\n        self.assertTrue(await self.daemon.jsonrpc_wallet_unlock(\"password\"))\n        await self.channel_create()\n\n    async def test_sync_with_encryption_and_password_change(self):\n        daemon, daemon2 = self.daemon, self.daemon2\n        wallet, wallet2 = daemon.wallet_manager.default_wallet, daemon2.wallet_manager.default_wallet\n\n        self.assertEqual(wallet2.encryption_password, None)\n        self.assertEqual(wallet2.encryption_password, None)\n\n        daemon.jsonrpc_wallet_encrypt('password')\n        self.assertEqual(wallet.encryption_password, 'password')\n\n        data = await daemon2.jsonrpc_sync_apply('password2')\n        # sync_apply doesn't save password if encrypt-on-disk is False\n        self.assertEqual(wallet2.encryption_password, None)\n\n        # Need to use new password2 in sync_apply. Attempts with other passwords\n        # should fail consistently with InvalidPasswordError.\n        random = Random('password')\n        for i in range(200):\n            bad_guess = ''.join(random.choices(string.digits + string.ascii_letters + string.punctuation, k=40))\n            self.assertNotEqual(bad_guess, 'password2')\n            with self.assertRaises(InvalidPasswordError):\n                await daemon.jsonrpc_sync_apply(bad_guess, data=data['data'], blocking=True)\n\n        await daemon.jsonrpc_sync_apply('password2', data=data['data'], blocking=True)\n        # sync_apply with new password2 also sets it as new local password\n        self.assertEqual(wallet.encryption_password, 'password2')\n        self.assertEqual(daemon.jsonrpc_wallet_status(), {'is_locked': False, 'is_encrypted': True,\n                                                          'is_syncing': True})\n        self.assertEqual(daemon.jsonrpc_preference_get(ENCRYPT_ON_DISK), {'encrypt-on-disk': True})\n        self.assertWalletEncrypted(wallet.storage.path, True)\n\n        # check new password is active\n        daemon.jsonrpc_wallet_lock()\n        self.assertFalse(await daemon.jsonrpc_wallet_unlock('password'))\n        self.assertTrue(await daemon.jsonrpc_wallet_unlock('password2'))\n\n        # propagate disk encryption to daemon2\n        data = await daemon.jsonrpc_sync_apply('password3')\n        # sync_apply (even with no data) on wallet with encrypt-on-disk updates local password\n        self.assertEqual(wallet.encryption_password, 'password3')\n        self.assertEqual(wallet2.encryption_password, None)\n        await daemon2.jsonrpc_sync_apply('password3', data=data['data'], blocking=True)\n        # the other device got new password and on disk encryption\n        self.assertEqual(wallet2.encryption_password, 'password3')\n        self.assertEqual(daemon2.jsonrpc_wallet_status(), {'is_locked': False, 'is_encrypted': True,\n                                                           'is_syncing': True})\n        self.assertEqual(daemon2.jsonrpc_preference_get(ENCRYPT_ON_DISK), {'encrypt-on-disk': True})\n        self.assertWalletEncrypted(wallet2.storage.path, True)\n\n        daemon2.jsonrpc_wallet_lock()\n        self.assertTrue(await daemon2.jsonrpc_wallet_unlock('password3'))\n\n    async def test_wallet_import_and_export(self):\n        daemon, daemon2 = self.daemon, self.daemon2\n\n        # Preferences\n        self.assertFalse(daemon.jsonrpc_preference_get())\n        self.assertFalse(daemon2.jsonrpc_preference_get())\n\n        daemon.jsonrpc_preference_set(\"fruit\", '[\"peach\", \"apricot\"]')\n        daemon.jsonrpc_preference_set(\"one\", \"1\")\n        daemon.jsonrpc_preference_set(\"conflict\", \"1\")\n        daemon2.jsonrpc_preference_set(\"another\", \"A\")\n        await asyncio.sleep(1)\n        # these preferences will win after merge since they are \"newer\"\n        daemon2.jsonrpc_preference_set(\"two\", \"2\")\n        daemon2.jsonrpc_preference_set(\"conflict\", \"2\")\n        daemon.jsonrpc_preference_set(\"another\", \"B\")\n\n        self.assertDictEqual(daemon.jsonrpc_preference_get(), {\n            \"one\": \"1\", \"conflict\": \"1\", \"another\": \"B\", \"fruit\": [\"peach\", \"apricot\"]\n        })\n        self.assertDictEqual(daemon2.jsonrpc_preference_get(), {\n            \"two\": \"2\", \"conflict\": \"2\", \"another\": \"A\"\n        })\n\n        self.assertItemCount(await daemon.jsonrpc_account_list(), 1)\n\n        data = await daemon2.jsonrpc_wallet_export(password='password')\n        await daemon.jsonrpc_wallet_import(data=data, password='password', blocking=True)\n\n        self.assertItemCount(await daemon.jsonrpc_account_list(), 2)\n        self.assertDictEqual(\n            # \"two\" key added and \"conflict\" value changed to \"2\"\n            daemon.jsonrpc_preference_get(),\n            {\"one\": \"1\", \"two\": \"2\", \"conflict\": \"2\", \"another\": \"B\", \"fruit\": [\"peach\", \"apricot\"]}\n        )\n\n        # Channel Certificate\n        # non-deterministic channel\n        self.daemon2.wallet_manager.default_account.channel_keys['mqs77XbdnuxWN4cXrjKbSoGLkvAHa4f4B8'] = (\n            '-----BEGIN EC PRIVATE KEY-----\\nMHQCAQEEIBZRTZ7tHnYCH3IE9mCo95'\n            '466L/ShYFhXGrjmSMFJw8eoAcGBSuBBAAK\\noUQDQgAEmucoPz9nI+ChZrfhnh'\n            '0RZ/bcX0r2G0pYBmoNKovtKzXGa8y07D66MWsW\\nqXptakqO/9KddIkBu5eJNS'\n            'UZzQCxPQ==\\n-----END EC PRIVATE KEY-----\\n'\n        )\n        channel = await self.create_nondeterministic_channel('@foo', '0.1', unhexlify(\n            '3056301006072a8648ce3d020106052b8104000a034200049ae7283f3f6723e0a1'\n            '66b7e19e1d1167f6dc5f4af61b4a58066a0d2a8bed2b35c66bccb4ec3eba316b16'\n            'a97a6d6a4a8effd29d748901bb9789352519cd00b13d'\n        ), self.daemon2, blocking=True)\n        await self.confirm_tx(channel['txid'], self.daemon2.ledger)\n\n        # both daemons will have the channel but only one has the cert so far\n        self.assertItemCount(await daemon.jsonrpc_channel_list(), 1)\n        self.assertEqual(len(daemon.wallet_manager.default_wallet.accounts[1].channel_keys), 0)\n        self.assertItemCount(await daemon2.jsonrpc_channel_list(), 1)\n        self.assertEqual(len(daemon2.wallet_manager.default_account.channel_keys), 1)\n\n        data = await daemon2.jsonrpc_wallet_export(password='password')\n        await daemon.jsonrpc_wallet_import(data=data, password='password', blocking=True)\n\n        # both daemons have the cert after sync'ing\n        self.assertEqual(\n            daemon2.wallet_manager.default_account.channel_keys,\n            daemon.wallet_manager.default_wallet.accounts[1].channel_keys\n        )\n\n        # test without passwords\n        data = await daemon2.jsonrpc_wallet_export()\n        json_data = json.loads(data)\n        self.assertEqual(json_data[\"name\"], \"Wallet\")\n        self.assertNotIn(\"four\", json_data[\"preferences\"])\n\n        json_data[\"preferences\"][\"four\"] = {\"value\": 4, \"ts\": 0}\n        await daemon.jsonrpc_wallet_import(data=json.dumps(json_data), blocking=True)\n        self.assertEqual(daemon.jsonrpc_preference_get(\"four\"), {\"four\": 4})\n\n        # if password is empty string, export is encrypted\n        data = await daemon2.jsonrpc_wallet_export(password=\"\")\n        self.assertNotEqual(data[0], \"{\")\n\n        # if password is empty string, import is decrypted\n        await daemon.jsonrpc_wallet_import(data, password=\"\")\n\n"
  },
  {
    "path": "tests/integration/blockchain/test_wallet_server_sessions.py",
    "content": "import asyncio\n\nfrom hub.herald import HUB_PROTOCOL_VERSION\nfrom hub.herald.session import LBRYElectrumX\n\nfrom lbry.error import InsufficientFundsError, ServerPaymentFeeAboveMaxAllowedError\nfrom lbry.wallet.network import ClientSession\nfrom lbry.wallet.rpc import RPCError\nfrom lbry.testcase import IntegrationTestCase, CommandTestCase\nfrom lbry.wallet.orchstr8.node import SPVNode\n\n\nclass TestSessions(IntegrationTestCase):\n    \"\"\"\n    Tests that server cleans up stale connections after session timeout and client times out too.\n    \"\"\"\n    async def test_session_bloat_from_socket_timeout(self):\n        await self.conductor.stop_spv()\n        await self.ledger.stop()\n        self.conductor.spv_node.session_timeout = 1\n        await self.conductor.start_spv()\n        session = ClientSession(\n            network=None, server=(self.conductor.spv_node.hostname, self.conductor.spv_node.port), timeout=0.2\n        )\n        await session.create_connection()\n        await session.send_request('server.banner', ())\n        self.assertEqual(len(self.conductor.spv_node.server.session_manager.sessions), 1)\n        self.assertFalse(session.is_closing())\n        await asyncio.sleep(1.1)\n        with self.assertRaises(asyncio.TimeoutError):\n            await session.send_request('server.banner', ())\n        self.assertTrue(session.is_closing())\n        self.assertEqual(len(self.conductor.spv_node.server.session_manager.sessions), 0)\n\n    async def test_proper_version(self):\n        info = await self.ledger.network.get_server_features()\n        self.assertEqual(HUB_PROTOCOL_VERSION, info['server_version'])\n\n    async def test_client_errors(self):\n        # Goal is ensuring thsoe are raised and not trapped accidentally\n        with self.assertRaisesRegex(Exception, 'not a valid address'):\n            await self.ledger.network.get_history('of the world')\n        with self.assertRaisesRegex(Exception, 'rejected by network rules.*TX decode failed'):\n            await self.ledger.network.broadcast('13370042004200')\n\n\nclass TestUsagePayment(CommandTestCase):\n    async def test_single_server_payment(self):\n        wallet_pay_service = self.daemon.component_manager.get_component('wallet_server_payments')\n        self.assertFalse(wallet_pay_service.running)\n        wallet_pay_service.payment_period = 0.5\n        # only starts with a positive max key fee\n        wallet_pay_service.max_fee = \"0.0\"\n        await wallet_pay_service.start(ledger=self.ledger, wallet=self.wallet)\n        self.assertFalse(wallet_pay_service.running)\n        wallet_pay_service.max_fee = \"1.0\"\n        await wallet_pay_service.start(ledger=self.ledger, wallet=self.wallet)\n        self.assertTrue(wallet_pay_service.running)\n        await wallet_pay_service.stop()\n        await wallet_pay_service.start(ledger=self.ledger, wallet=self.wallet)\n\n        address = await self.blockchain.get_raw_change_address()\n        _, history = await self.ledger.get_local_status_and_history(address)\n        self.assertEqual(history, [])\n\n        node = SPVNode(node_number=2)\n        await node.start(self.blockchain, extraconf={\"payment_address\": address, \"daily_fee\": \"1.1\"})\n        self.addCleanup(node.stop)\n        self.daemon.jsonrpc_settings_set('lbryum_servers', [f\"{node.hostname}:{node.port}\"])\n        await self.daemon.jsonrpc_wallet_reconnect()\n        LBRYElectrumX.set_server_features(node.server.env)\n        features = await self.ledger.network.get_server_features()\n        self.assertEqual(features[\"payment_address\"], address)\n        self.assertEqual(features[\"daily_fee\"], \"1.1\")\n        with self.assertRaises(ServerPaymentFeeAboveMaxAllowedError):\n            await asyncio.wait_for(wallet_pay_service.on_payment.first, timeout=30)\n        node.server.env.daily_fee = \"1.0\"\n        node.server.env.payment_address = address\n        LBRYElectrumX.set_server_features(node.server.env)\n        # self.daemon.jsonrpc_settings_set('lbryum_servers', [f\"{node.hostname}:{node.port}\"])\n        await self.daemon.jsonrpc_wallet_reconnect()\n        features = await self.ledger.network.get_server_features()\n        self.assertEqual(features[\"payment_address\"], address)\n        self.assertEqual(features[\"daily_fee\"], \"1.0\")\n        tx = await asyncio.wait_for(wallet_pay_service.on_payment.first, timeout=30)\n        self.assertIsNotNone(await self.blockchain.get_raw_transaction(tx.id))  # verify its broadcasted\n        self.assertEqual(tx.outputs[0].amount, 100000000)\n        self.assertEqual(tx.outputs[0].get_address(self.ledger), address)\n\n        # continue paying until account is out of funds\n        with self.assertRaises(InsufficientFundsError):\n            for i in range(10):\n                await asyncio.wait_for(wallet_pay_service.on_payment.first, timeout=30)\n        self.assertTrue(wallet_pay_service.running)\n\nclass TestESSync(CommandTestCase):\n    async def test_es_sync_utility(self):\n        es_writer = self.conductor.spv_node.es_writer\n        server_search_client = self.conductor.spv_node.server.session_manager.search_index\n\n        for i in range(10):\n            await self.stream_create(f\"stream{i}\", bid='0.001')\n        await self.generate(1)\n        self.assertEqual(10, len(await self.claim_search(order_by=['height'])))\n\n        # delete the index and verify nothing is returned by claim search\n        await es_writer.delete_index()\n        server_search_client.clear_caches()\n        self.assertEqual(0, len(await self.claim_search(order_by=['height'])))\n\n        # reindex, 10 claims should be returned\n        await es_writer.reindex(force=True)\n        self.assertEqual(10, len(await self.claim_search(order_by=['height'])))\n        server_search_client.clear_caches()\n        self.assertEqual(10, len(await self.claim_search(order_by=['height'])))\n\n        # reindex again, this should not appear to do anything but will delete and reinsert the same 10 claims\n        await es_writer.reindex(force=True)\n        self.assertEqual(10, len(await self.claim_search(order_by=['height'])))\n        server_search_client.clear_caches()\n        self.assertEqual(10, len(await self.claim_search(order_by=['height'])))\n\n        # delete the index again and stop the writer, upon starting it the writer should reindex automatically\n        await es_writer.delete_index()\n        await es_writer.stop()\n        server_search_client.clear_caches()\n        self.assertEqual(0, len(await self.claim_search(order_by=['height'])))\n\n        await es_writer.start(reindex=True)\n        self.assertEqual(10, len(await self.claim_search(order_by=['height'])))\n\n        # stop the es writer and advance the chain by 1, adding a new claim. upon resuming the es writer, it should\n        # add the new claim\n        await es_writer.stop()\n\n        stream11 = self.get_claim_id(await self.stream_create(f\"stream11\", bid='0.001', confirm=False))\n        current_height = self.conductor.spv_node.writer.height\n        generate_block_task = asyncio.create_task(self.generate(1))\n        await self.conductor.spv_node.writer.wait_until_block(current_height + 1)\n\n        await es_writer.start()\n        await generate_block_task\n        self.assertEqual(11, len(await self.claim_search(order_by=['height'])))\n\n        # stop/delete es and advance the chain by 1, removing stream11\n        await es_writer.delete_index()\n        await es_writer.stop()\n        server_search_client.clear_caches()\n        await self.stream_abandon(stream11, confirm=False)\n        current_height = self.conductor.spv_node.writer.height\n        generate_block_task = asyncio.create_task(self.generate(1))\n        await self.conductor.spv_node.writer.wait_until_block(current_height + 1)\n        await es_writer.start(reindex=True)\n        await generate_block_task\n        self.assertEqual(10, len(await self.claim_search(order_by=['height'])))\n\n\n    # # this time we will test a migration from unversioned to v1\n        # await db.search_index.sync_client.indices.delete_template(db.search_index.index)\n        # await db.search_index.stop()\n        #\n        # await make_es_index_and_run_sync(env, db=db, index_name=db.search_index.index, force=True)\n        # await db.search_index.start()\n        #\n        # await es_writer.reindex()\n        # self.assertEqual(10, len(await self.claim_search(order_by=['height'])))\n\n\nclass TestHubDiscovery(CommandTestCase):\n\n    async def test_hub_discovery(self):\n        us_final_node = SPVNode(node_number=2)\n        await us_final_node.start(self.blockchain, extraconf={\"country\": \"US\"})\n        self.addCleanup(us_final_node.stop)\n        final_node_host = f\"{us_final_node.hostname}:{us_final_node.port}\"\n\n        kp_final_node = SPVNode(node_number=3)\n        await kp_final_node.start(self.blockchain, extraconf={\"country\": \"KP\"})\n        self.addCleanup(kp_final_node.stop)\n        kp_final_node_host = f\"{kp_final_node.hostname}:{kp_final_node.port}\"\n\n        relay_node = SPVNode(node_number=4)\n        await relay_node.start(self.blockchain, extraconf={\n            \"country\": \"FR\",\n            \"peer_hubs\": \",\".join([kp_final_node_host, final_node_host])\n        })\n        relay_node_host = f\"{relay_node.hostname}:{relay_node.port}\"\n        self.addCleanup(relay_node.stop)\n\n        self.assertEqual(list(self.daemon.conf.known_hubs), [])\n        self.assertEqual(\n            self.daemon.ledger.network.client.server_address_and_port,\n            ('127.0.0.1', 50002)\n        )\n\n        # connect to relay hub which will tell us about the final hubs\n        self.daemon.jsonrpc_settings_set('lbryum_servers', [relay_node_host])\n        await self.daemon.jsonrpc_wallet_reconnect()\n        self.assertEqual(\n            self.daemon.conf.known_hubs.filter(), {\n                (relay_node.hostname, relay_node.port): {\"country\": \"FR\"},\n                (us_final_node.hostname, us_final_node.port): {},  # discovered from relay but not contacted yet\n                (kp_final_node.hostname, kp_final_node.port): {},  # discovered from relay but not contacted yet\n            }\n        )\n        self.assertEqual(\n            self.daemon.ledger.network.client.server_address_and_port, ('127.0.0.1', relay_node.port)\n        )\n\n        # use known_hubs to connect to final US hub\n        self.daemon.jsonrpc_settings_clear('lbryum_servers')\n        self.daemon.conf.jurisdiction = \"US\"\n        await self.daemon.jsonrpc_wallet_reconnect()\n        self.assertEqual(\n            self.daemon.conf.known_hubs.filter(), {\n                (relay_node.hostname, relay_node.port): {\"country\": \"FR\"},\n                (us_final_node.hostname, us_final_node.port): {\"country\": \"US\"},\n                (kp_final_node.hostname, kp_final_node.port): {\"country\": \"KP\"},\n            }\n        )\n        self.assertEqual(\n            self.daemon.ledger.network.client.server_address_and_port, ('127.0.0.1', us_final_node.port)\n        )\n\n        # connection to KP jurisdiction\n        self.daemon.conf.jurisdiction = \"KP\"\n        await self.daemon.jsonrpc_wallet_reconnect()\n        self.assertEqual(\n            self.daemon.ledger.network.client.server_address_and_port, ('127.0.0.1', kp_final_node.port)\n        )\n\n        kp_final_node.server.session_manager._notify_peer('127.0.0.1:9988')\n        await self.daemon.ledger.network.on_hub.first\n        await asyncio.sleep(0.5)  # wait for above event to be processed by other listeners\n        self.assertEqual(\n            self.daemon.conf.known_hubs.filter(), {\n                (relay_node.hostname, relay_node.port): {\"country\": \"FR\"},\n                (us_final_node.hostname, us_final_node.port): {\"country\": \"US\"},\n                (kp_final_node.hostname, kp_final_node.port): {\"country\": \"KP\"},\n                ('127.0.0.1', 9988): {}\n            }\n        )\n\n\nclass TestStressFlush(CommandTestCase):\n    # async def test_flush_over_66_thousand(self):\n    #     history = self.conductor.spv_node.server.db.history\n    #     history.flush_count = 66_000\n    #     history.flush()\n    #     self.assertEqual(history.flush_count, 66_001)\n    #     await self.generate(1)\n    #     self.assertEqual(history.flush_count, 66_002)\n\n    async def test_thousands_claim_ids_on_search(self):\n        await self.stream_create()\n        with self.assertRaises(RPCError) as err:\n            await self.claim_search(not_channel_ids=[(\"%040x\" % i) for i in range(8196)])\n        # in the go hub this doesnt have a `.` at the end, in python it does\n        self.assertTrue(err.exception.message.startswith('not_channel_ids cant have more than 2048 items'))\n"
  },
  {
    "path": "tests/integration/claims/__init__.py",
    "content": ""
  },
  {
    "path": "tests/integration/claims/test_claim_commands.py",
    "content": "import os.path\nimport tempfile\nimport logging\nimport asyncio\nfrom binascii import unhexlify\nfrom unittest import skip\nfrom urllib.request import urlopen\nimport ecdsa\n\nfrom lbry.error import InsufficientFundsError\n\nfrom lbry.extras.daemon.daemon import DEFAULT_PAGE_SIZE\nfrom lbry.testcase import CommandTestCase\nfrom lbry.wallet.orchstr8.node import SPVNode\nfrom lbry.wallet.transaction import Transaction, Output\nfrom lbry.wallet.util import satoshis_to_coins as lbc\nfrom lbry.crypto.hash import sha256\n\nlog = logging.getLogger(__name__)\n\n\nSTREAM_TYPES = {\n    'video': 1,\n    'audio': 2,\n    'image': 3,\n    'document': 4,\n    'binary': 5,\n    'model': 6,\n}\n\n\ndef verify(channel, data, signature, channel_hash=None):\n    pieces = [\n        signature['salt'].encode(),\n        channel_hash or channel.claim_hash,\n        data\n    ]\n    return Output.is_signature_valid(\n        unhexlify(signature['signature']),\n        sha256(b''.join(pieces)),\n        channel.claim.channel.public_key_bytes\n    )\n\n\nclass ClaimTestCase(CommandTestCase):\n\n    files_directory = os.path.join(os.path.dirname(__file__), 'files')\n    video_file_url = 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4'\n    video_file_name = os.path.join(files_directory, 'ForBiggerEscapes.mp4')\n    image_data = unhexlify(\n        b'89504e470d0a1a0a0000000d49484452000000050000000708020000004fc'\n        b'510b9000000097048597300000b1300000b1301009a9c1800000015494441'\n        b'5408d763fcffff3f031260624005d4e603004c45030b5286e9ea000000004'\n        b'9454e44ae426082'\n    )\n\n    def setUp(self):\n        if not os.path.exists(self.video_file_name):\n            if not os.path.exists(self.files_directory):\n                os.mkdir(self.files_directory)\n            log.info(f'downloading test video from {self.video_file_name}')\n            with urlopen(self.video_file_url) as response, \\\n                    open(self.video_file_name, 'wb') as video_file:\n                video_file.write(response.read())\n\n\nclass ClaimSearchCommand(ClaimTestCase):\n\n    async def create_channel(self):\n        self.channel = await self.channel_create('@abc', '1.0')\n        self.channel_id = self.get_claim_id(self.channel)\n\n    async def create_lots_of_streams(self):\n        tx = await self.daemon.jsonrpc_account_fund(None, None, '0.001', outputs=100, broadcast=True)\n        await self.confirm_tx(tx.id)\n        # 4 claims per block, 3 blocks. Sorted by height (descending) then claim name (ascending).\n        self.streams = []\n        for j in range(4):\n            same_height_claims = []\n            for k in range(5):\n                claim_tx = await self.stream_create(\n                    f'c{j}-{k}', '0.000001', channel_id=self.channel_id, confirm=False)\n                same_height_claims.append(claim_tx['outputs'][0]['name'])\n                await self.on_transaction_dict(claim_tx)\n            claim_tx = await self.stream_create(\n                f'c{j}-6', '0.000001', channel_id=self.channel_id, confirm=True)\n            same_height_claims.append(claim_tx['outputs'][0]['name'])\n            self.streams = same_height_claims + self.streams\n\n    async def assertFindsClaim(self, claim, **kwargs):\n        await self.assertFindsClaims([claim], **kwargs)\n\n    async def assertFindsClaims(self, claims, **kwargs):\n        kwargs.setdefault('order_by', ['height', '^name'])\n        results = await self.claim_search(**kwargs)\n        self.assertEqual(\n            len(claims), len(results),\n            f\"{[claim['outputs'][0]['name'] for claim in claims]} != {[result['name'] for result in results]}\")\n        for claim, result in zip(claims, results):\n            self.assertEqual(\n                (claim['txid'], self.get_claim_id(claim)),\n                (result['txid'], result['claim_id']),\n                f\"(expected {claim['outputs'][0]['name']}) != (got {result['name']})\"\n            )\n\n    async def assertListsClaims(self, claims, **kwargs):\n        kwargs.setdefault('order_by', 'height')\n        results = await self.claim_list(**kwargs)\n        self.assertEqual(len(claims), len(results))\n        for claim, result in zip(claims, results):\n            self.assertEqual(\n                (claim['txid'], self.get_claim_id(claim)),\n                (result['txid'], result['claim_id']),\n                f\"(expected {claim['outputs'][0]['name']}) != (got {result['name']})\"\n            )\n\n    @skip(\"doesnt happen on ES...?\")\n    async def test_disconnect_on_memory_error(self):\n        claim_ids = [\n            '0000000000000000000000000000000000000000',\n        ] * 23828\n        self.assertListEqual([], await self.claim_search(claim_ids=claim_ids))\n\n        # this should do nothing... if the resolve (which is retried) results in the server disconnecting,\n        # it kerplodes\n        await asyncio.wait_for(self.daemon.jsonrpc_resolve([\n            f'0000000000000000000000000000000000000000{i}' for i in range(30000)\n        ]), 30)\n\n        # 23829 claim ids makes the request just large enough\n        claim_ids = [\n            '0000000000000000000000000000000000000000',\n        ] * 33829\n        with self.assertRaises(ConnectionResetError):\n            await self.claim_search(claim_ids=claim_ids)\n\n    async def test_basic_claim_search(self):\n        await self.create_channel()\n        channel_txo = self.channel['outputs'][0]\n        channel2 = await self.channel_create('@abc', '0.1', allow_duplicate_name=True)\n        channel_txo2 = channel2['outputs'][0]\n        channel_id2 = channel_txo2['claim_id']\n\n        # finding a channel\n        await self.assertFindsClaims([channel2, self.channel], name='@abc')\n        await self.assertFindsClaim(self.channel, name='@abc', is_controlling=True)\n        await self.assertFindsClaim(self.channel, claim_id=self.channel_id)\n        await self.assertFindsClaim(self.channel, txid=self.channel['txid'], nout=0)\n        await self.assertFindsClaim(channel2, claim_id=channel_id2)\n        await self.assertFindsClaim(channel2, txid=channel2['txid'], nout=0)\n        await self.assertFindsClaim(\n            channel2, public_key_id=channel_txo2['value']['public_key_id'])\n        await self.assertFindsClaim(\n            self.channel, public_key_id=channel_txo['value']['public_key_id'])\n\n        signed = await self.stream_create('on-channel-claim', '0.001', channel_id=self.channel_id)\n        signed2 = await self.stream_create('on-channel-claim', '0.0001', channel_id=channel_id2,\n                                           allow_duplicate_name=True)\n        unsigned = await self.stream_create('unsigned', '0.0001')\n\n        # finding claims with and without a channel\n        await self.assertFindsClaims([signed2, signed], name='on-channel-claim')\n        await self.assertFindsClaims([signed2, signed], channel_ids=[self.channel_id, channel_id2])\n        await self.assertFindsClaim(signed, name='on-channel-claim', channel_ids=[self.channel_id])\n        await self.assertFindsClaim(signed2, name='on-channel-claim', channel_ids=[channel_id2])\n        await self.assertFindsClaim(unsigned, name='unsigned')\n        await self.assertFindsClaim(unsigned, txid=unsigned['txid'], nout=0)\n        await self.assertFindsClaim(unsigned, claim_id=self.get_claim_id(unsigned))\n\n        two = await self.stream_create('on-channel-claim-2', '0.0001', channel_id=self.channel_id)\n        three = await self.stream_create('on-channel-claim-3', '0.0001', channel_id=self.channel_id)\n\n        # three streams in channel, zero streams in abandoned channel\n        claims = [three, two, signed]\n        await self.assertFindsClaims(claims, channel_ids=[self.channel_id])\n        await self.assertFindsClaims(claims, channel=f\"@abc#{self.channel_id}\")\n        await self.assertFindsClaims(claims, channel=f\"@abc#{self.channel_id}\", valid_channel_signature=True)\n        await self.assertFindsClaims(claims, channel=f\"@abc#{self.channel_id}\", has_channel_signature=True, valid_channel_signature=True)\n        await self.assertFindsClaims([], channel=f\"@abc#{self.channel_id}\", has_channel_signature=True, invalid_channel_signature=True)  # fixme\n        await self.assertFindsClaims([], channel=f\"@inexistent\")\n        await self.assertFindsClaims([three, two, signed2, signed], channel_ids=[channel_id2, self.channel_id])\n        await self.channel_abandon(claim_id=self.channel_id)\n        await self.assertFindsClaims([], channel=f\"@abc#{self.channel_id}\", valid_channel_signature=True)\n        await self.assertFindsClaims([], channel_ids=[self.channel_id], valid_channel_signature=True)\n        await self.assertFindsClaims([signed2], channel_ids=[channel_id2], valid_channel_signature=True)\n        # pass `invalid_channel_signature=False` to catch a bug in argument processing\n        await self.assertFindsClaims([signed2], channel_ids=[channel_id2, self.channel_id],\n                                     valid_channel_signature=True, invalid_channel_signature=False)\n        # invalid signature still returns channel_id\n        self.ledger._tx_cache.clear()\n        invalid_claims = await self.claim_search(invalid_channel_signature=True, has_channel_signature=True)\n        self.assertEqual(3, len(invalid_claims))\n        self.assertTrue(all([not c['is_channel_signature_valid'] for c in invalid_claims]))\n        self.assertEqual({'channel_id': self.channel_id}, invalid_claims[0]['signing_channel'])\n\n        valid_claims = await self.claim_search(valid_channel_signature=True, has_channel_signature=True)\n        self.assertEqual(1, len(valid_claims))\n        self.assertTrue(all([c['is_channel_signature_valid'] for c in valid_claims]))\n        self.assertEqual('@abc', valid_claims[0]['signing_channel']['name'])\n\n        # abandoned stream won't show up for streams in channel search\n        await self.stream_abandon(txid=signed2['txid'], nout=0)\n        await self.assertFindsClaims([], channel_ids=[channel_id2])\n        # resolve by claim ids\n        await self.assertFindsClaims([three, two], claim_ids=[self.get_claim_id(three), self.get_claim_id(two)])\n        await self.assertFindsClaims([three], claim_id=self.get_claim_id(three))\n        await self.assertFindsClaims([three], claim_id=self.get_claim_id(three), text='*')\n        # resolve by sd hash\n        two_sd_hash = two['outputs'][0]['value']['source']['sd_hash']\n        await self.assertFindsClaims([two], sd_hash=two_sd_hash)\n        await self.assertFindsClaims([two], sd_hash=two_sd_hash[:4])\n\n    async def test_source_filter(self):\n        channel = await self.channel_create('@abc')\n        no_source = await self.stream_create('no-source', data=None)\n        normal = await self.stream_create('normal', data=b'normal')\n        normal_repost = await self.stream_repost(self.get_claim_id(normal), 'normal-repost')\n        no_source_repost = await self.stream_repost(self.get_claim_id(no_source), 'no-source-repost')\n        channel_repost = await self.stream_repost(self.get_claim_id(channel), 'channel-repost')\n        await self.assertFindsClaims([channel_repost, no_source_repost, no_source, channel], has_no_source=True)\n        await self.assertListsClaims([no_source, channel], has_no_source=True)\n        await self.assertFindsClaims([channel_repost, normal_repost, normal, channel], has_source=True)\n        await self.assertListsClaims([channel_repost, no_source_repost, normal_repost, normal], has_source=True)\n        await self.assertFindsClaims([channel_repost, no_source_repost, normal_repost, normal, no_source, channel])\n        await self.assertListsClaims([channel_repost, no_source_repost, normal_repost, normal, no_source, channel])\n        await self.assertFindsClaims([normal_repost, normal], stream_types=list(STREAM_TYPES.keys()))\n\n    async def test_pagination(self):\n        await self.create_channel()\n        await self.create_lots_of_streams()\n\n        # with and without totals\n        results = await self.daemon.jsonrpc_claim_search()\n        self.assertEqual(results['total_pages'], 2)\n        self.assertEqual(results['total_items'], 25)\n        results = await self.daemon.jsonrpc_claim_search(no_totals=True)\n        self.assertNotIn('total_pages', results)\n        self.assertNotIn('total_items', results)\n\n        # defaults\n        page = await self.claim_search(channel='@abc', order_by=['height', '^name'])\n        page_claim_ids = [item['name'] for item in page]\n        self.assertEqual(page_claim_ids, self.streams[:DEFAULT_PAGE_SIZE])\n\n        # page with default page_size\n        page = await self.claim_search(page=2, channel='@abc', order_by=['height', '^name'])\n        page_claim_ids = [item['name'] for item in page]\n        self.assertEqual(page_claim_ids, self.streams[DEFAULT_PAGE_SIZE:(DEFAULT_PAGE_SIZE*2)])\n\n        # page_size larger than dataset\n        page = await self.claim_search(page_size=50, channel='@abc', order_by=['height', '^name'])\n        page_claim_ids = [item['name'] for item in page]\n        self.assertEqual(page_claim_ids, self.streams)\n\n        # page_size less than dataset\n        page = await self.claim_search(page_size=6, channel='@abc', order_by=['height', '^name'])\n        page_claim_ids = [item['name'] for item in page]\n        self.assertEqual(page_claim_ids, self.streams[:6])\n\n        # page and page_size\n        page = await self.claim_search(page=2, page_size=6, channel='@abc', order_by=['height', '^name'])\n        page_claim_ids = [item['name'] for item in page]\n        self.assertEqual(page_claim_ids, self.streams[6:12])\n\n        out_of_bounds = await self.claim_search(page=4, page_size=20, channel='@abc')\n        self.assertEqual(out_of_bounds, [])\n\n    async def test_tag_search(self):\n        claim1 = await self.stream_create('claim1', tags=['aBc'])\n        claim2 = await self.stream_create('claim2', tags=['#abc', 'def'])\n        claim3 = await self.stream_create('claim3', tags=['abc', 'ghi', 'jkl'])\n        claim4 = await self.stream_create('claim4', tags=['abc\\t', 'ghi', 'mno'])\n        claim5 = await self.stream_create('claim5', tags=['pqr'])\n\n        # any_tags\n        await self.assertFindsClaims([claim5, claim4, claim3, claim2, claim1], any_tags=['\\tabc', 'pqr'])\n        await self.assertFindsClaims([claim4, claim3, claim2, claim1], any_tags=['abc'])\n        await self.assertFindsClaims([claim4, claim3, claim2, claim1], any_tags=['abc', 'ghi'])\n        await self.assertFindsClaims([claim4, claim3], any_tags=['ghi'])\n        await self.assertFindsClaims([claim4, claim3], any_tags=['ghi', 'xyz'])\n        await self.assertFindsClaims([], any_tags=['xyz'])\n\n        # all_tags\n        await self.assertFindsClaims([], all_tags=['abc', 'pqr'])\n        await self.assertFindsClaims([claim4, claim3, claim2, claim1], all_tags=['ABC'])\n        await self.assertFindsClaims([claim4, claim3], all_tags=['abc', 'ghi'])\n        await self.assertFindsClaims([claim4, claim3], all_tags=['ghi'])\n        await self.assertFindsClaims([], all_tags=['ghi', 'xyz'])\n        await self.assertFindsClaims([], all_tags=['xyz'])\n\n        # not_tags\n        await self.assertFindsClaims([], not_tags=['abc', 'pqr'])\n        await self.assertFindsClaims([claim5], not_tags=['abC'])\n        await self.assertFindsClaims([claim5], not_tags=['abc', 'ghi'])\n        await self.assertFindsClaims([claim5, claim2, claim1], not_tags=['ghi'])\n        await self.assertFindsClaims([claim5, claim2, claim1], not_tags=['ghi', 'xyz'])\n        await self.assertFindsClaims([claim5, claim4, claim3, claim2, claim1], not_tags=['xyz'])\n\n        # combinations\n        await self.assertFindsClaims([claim3], all_tags=['abc', 'ghi'], not_tags=['mno'])\n        await self.assertFindsClaims([claim3], all_tags=['abc', 'ghi'], any_tags=['jkl'], not_tags=['mno'])\n        await self.assertFindsClaims([claim4, claim3, claim2], all_tags=['abc'], any_tags=['def', 'ghi'])\n\n    async def test_order_by(self):\n        height = self.ledger.network.remote_height\n        claims = [await self.stream_create(f'claim{i}') for i in range(5)]\n\n        await self.assertFindsClaims(claims, order_by=[\"^height\"])\n        await self.assertFindsClaims(list(reversed(claims)), order_by=[\"height\"])\n\n        await self.assertFindsClaims([claims[0]], height=height+1)\n        await self.assertFindsClaims([claims[4]], height=height+5)\n        await self.assertFindsClaims(claims[:1], height=f'<{height+2}', order_by=[\"^height\"])\n        await self.assertFindsClaims(claims[:2], height=f'<={height+2}', order_by=[\"^height\"])\n        await self.assertFindsClaims(claims[2:], height=f'>{height+2}', order_by=[\"^height\"])\n        await self.assertFindsClaims(claims[1:], height=f'>={height+2}', order_by=[\"^height\"])\n\n        await self.assertFindsClaims(claims, order_by=[\"^name\"])\n\n    async def test_search_by_fee(self):\n        claim1 = await self.stream_create('claim1', fee_amount='1.0', fee_currency='lbc')\n        claim2 = await self.stream_create('claim2', fee_amount='0.9', fee_currency='lbc')\n        claim3 = await self.stream_create('claim3', fee_amount='0.5', fee_currency='lbc')\n        claim4 = await self.stream_create('claim4', fee_amount='0.1', fee_currency='lbc')\n        claim5 = await self.stream_create('claim5', fee_amount='1.0', fee_currency='usd')\n        repost1 = await self.stream_repost(self.get_claim_id(claim1), 'repost1')\n        repost5 = await self.stream_repost(self.get_claim_id(claim5), 'repost5')\n\n        await self.assertFindsClaims([repost5, repost1, claim5, claim4, claim3, claim2, claim1], fee_amount='>0')\n        await self.assertFindsClaims([repost1, claim4, claim3, claim2, claim1], fee_currency='lbc')\n        await self.assertFindsClaims([repost1, claim3, claim2, claim1], fee_amount='>0.1', fee_currency='lbc')\n        await self.assertFindsClaims([claim4, claim3, claim2], fee_amount='<1.0', fee_currency='lbc')\n        await self.assertFindsClaims([claim3], fee_amount='0.5', fee_currency='lbc')\n        await self.assertFindsClaims([repost5, claim5], fee_currency='usd')\n\n    async def test_search_by_language(self):\n        claim1 = await self.stream_create('claim1', fee_amount='1.0', fee_currency='lbc')\n        claim2 = await self.stream_create('claim2', fee_amount='0.9', fee_currency='lbc')\n        claim3 = await self.stream_create('claim3', fee_amount='0.5', fee_currency='lbc', languages='en')\n        claim4 = await self.stream_create('claim4', fee_amount='0.1', fee_currency='lbc', languages='en')\n        claim5 = await self.stream_create('claim5', fee_amount='1.0', fee_currency='usd', languages='es')\n\n        await self.assertFindsClaims([claim4, claim3], any_languages=['en'])\n        await self.assertFindsClaims([claim2, claim1], any_languages=['none'])\n        await self.assertFindsClaims([claim4, claim3, claim2, claim1], any_languages=['none', 'en'])\n        await self.assertFindsClaims([claim5], any_languages=['es'])\n        await self.assertFindsClaims([claim5, claim4, claim3], any_languages=['en', 'es'])\n        await self.assertFindsClaims([], fee_currency='foo')\n\n    async def test_search_by_channel(self):\n        match = self.assertFindsClaims\n\n        chan1_id = self.get_claim_id(await self.channel_create('@chan1'))\n        chan2_id = self.get_claim_id(await self.channel_create('@chan2'))\n        chan3_id = self.get_claim_id(await self.channel_create('@chan3'))\n        chan4 = await self.channel_create('@chan4', '0.1')\n\n        claim1 = await self.stream_create('claim1')\n        claim2 = await self.stream_create('claim2', channel_id=chan1_id)\n        claim3 = await self.stream_create('claim3', channel_id=chan1_id)\n        claim4 = await self.stream_create('claim4', channel_id=chan2_id)\n        claim5 = await self.stream_create('claim5', channel_id=chan2_id)\n        claim6 = await self.stream_create('claim6', channel_id=chan3_id)\n        await self.channel_abandon(chan3_id)\n\n        # {has/valid/invalid}_channel_signature\n        await match([claim6, claim5, claim4, claim3, claim2], has_channel_signature=True)\n        await match([claim5, claim4, claim3, claim2, claim1], valid_channel_signature=True, claim_type='stream')\n        await match([claim6, claim1],                         invalid_channel_signature=True, claim_type='stream')\n        await match([claim5, claim4, claim3, claim2], has_channel_signature=True, valid_channel_signature=True)\n        await match([claim6],                         has_channel_signature=True, invalid_channel_signature=True)\n\n        # not_channel_ids\n        await match([claim6, claim5, claim4, claim3, claim2, claim1], not_channel_ids=['abc123'], claim_type='stream')\n        await match([claim5, claim4, claim3, claim2, claim1],         not_channel_ids=[chan3_id], claim_type='stream')\n        await match([claim6, claim5, claim4, claim1],                 not_channel_ids=[chan1_id], claim_type='stream')\n        await match([claim6, claim3, claim2, claim1],                 not_channel_ids=[chan2_id], claim_type='stream')\n        await match([claim6, claim1],                       not_channel_ids=[chan1_id, chan2_id], claim_type='stream')\n        await match([claim6, claim1, chan4],                not_channel_ids=[chan1_id, chan2_id])\n\n        # not_channel_ids + valid_channel_signature\n        await match([claim5, claim4, claim3, claim2, claim1],\n                    not_channel_ids=['abc123'], valid_channel_signature=True, claim_type='stream')\n        await match([claim5, claim4, claim1],\n                    not_channel_ids=[chan1_id], valid_channel_signature=True, claim_type='stream')\n        await match([claim3, claim2, claim1],\n                    not_channel_ids=[chan2_id], valid_channel_signature=True, claim_type='stream')\n        await match([claim1], not_channel_ids=[chan1_id, chan2_id], valid_channel_signature=True, claim_type='stream')\n\n        # not_channel_ids + has_channel_signature\n        await match([claim6, claim5, claim4, claim3, claim2], not_channel_ids=['abc123'], has_channel_signature=True)\n        await match([claim6, claim5, claim4],                 not_channel_ids=[chan1_id], has_channel_signature=True)\n        await match([claim6, claim3, claim2],                 not_channel_ids=[chan2_id], has_channel_signature=True)\n        await match([claim6],                       not_channel_ids=[chan1_id, chan2_id], has_channel_signature=True)\n\n        # not_channel_ids + has_channel_signature + valid_channel_signature\n        await match([claim5, claim4, claim3, claim2],\n                    not_channel_ids=['abc123'], has_channel_signature=True, valid_channel_signature=True)\n        await match([claim5, claim4],\n                    not_channel_ids=[chan1_id], has_channel_signature=True, valid_channel_signature=True)\n        await match([claim3, claim2],\n                    not_channel_ids=[chan2_id], has_channel_signature=True, valid_channel_signature=True)\n        await match([], not_channel_ids=[chan1_id, chan2_id], has_channel_signature=True, valid_channel_signature=True)\n\n    @skip\n    async def test_no_source_and_valid_channel_signature_and_media_type(self):\n        await self.channel_create('@spam2', '1.0')\n        await self.stream_create('barrrrrr', '1.0', channel_name='@spam2', file_path=self.video_file_name)\n        paradox_no_source_claims = await self.claim_search(has_no_source=True, valid_channel_signature=True,\n                                                   media_type=\"video/mp4\")\n        mp4_claims = await self.claim_search(media_type=\"video/mp4\")\n        no_source_claims = await self.claim_search(has_no_source=True, valid_channel_signature=True)\n        self.assertEqual(0, len(paradox_no_source_claims))\n        self.assertEqual(1, len(no_source_claims))\n        self.assertEqual(1, len(mp4_claims))\n\n    async def test_limit_claims_per_channel(self):\n        match = self.assertFindsClaims\n        chan1_id = self.get_claim_id(await self.channel_create('@chan1'))\n        chan2_id = self.get_claim_id(await self.channel_create('@chan2'))\n        claim1 = await self.stream_create('claim1')\n        claim2 = await self.stream_create('claim2', channel_id=chan1_id)\n        claim3 = await self.stream_create('claim3', channel_id=chan1_id)\n        claim4 = await self.stream_create('claim4', channel_id=chan1_id)\n        claim5 = await self.stream_create('claim5', channel_id=chan2_id)\n        claim6 = await self.stream_create('claim6', channel_id=chan2_id)\n        await match(\n            [claim6, claim5, claim4, claim3, claim1],\n            limit_claims_per_channel=2, claim_type='stream'\n        )\n        await match(\n            [claim6, claim5, claim4, claim3, claim2, claim1],\n            limit_claims_per_channel=3, claim_type='stream'\n        )\n\n    async def test_no_duplicates(self):\n        await self.generate(10)\n        match = self.assertFindsClaims\n        claims = []\n        channels = []\n        first = await self.stream_create('original_claim0')\n        second = await self.stream_create('original_claim1')\n        for i in range(10):\n            repost_id = self.get_claim_id(second if i % 2 == 0 else first)\n            channel = await self.channel_create(f'@chan{i}', bid='0.001')\n            channels.append(channel)\n            claims.append(\n                await self.stream_repost(repost_id, f'claim{i}', bid='0.001', channel_id=self.get_claim_id(channel)))\n        await match([first, second] + channels,\n                    remove_duplicates=True, order_by=['^height'])\n        await match(list(reversed(channels)) + [second, first],\n                    remove_duplicates=True, order_by=['height'])\n        # the original claims doesn't show up, so we pick the oldest reposts\n        await match([channels[0], claims[0], channels[1], claims[1]] + channels[2:],\n                    height='>218',\n                    remove_duplicates=True, order_by=['^height'])\n        # limit claims per channel, invert order, oldest ones are still chosen\n        await match(channels[2:][::-1] + [claims[1], channels[1], claims[0], channels[0]],\n                    height='>218', limit_claims_per_channel=1,\n                    remove_duplicates=True, order_by=['height'])\n\n    async def test_limit_claims_per_channel_across_sorted_pages(self):\n        await self.generate(10)\n        match = self.assertFindsClaims\n        channel_id = self.get_claim_id(await self.channel_create('@chan0'))\n        claims = []\n        first = await self.stream_create('claim0', channel_id=channel_id)\n        second = await self.stream_create('claim1', channel_id=channel_id)\n        for i in range(2, 10):\n            some_chan = self.get_claim_id(await self.channel_create(f'@chan{i}', bid='0.001'))\n            claims.append(await self.stream_create(f'claim{i}', bid='0.001', channel_id=some_chan))\n        last = await self.stream_create('claim10', channel_id=channel_id)\n\n        await match(\n            [first, second, claims[0], claims[1]], page_size=4,\n            limit_claims_per_channel=3, claim_type='stream', order_by=['^height']\n        )\n        # second goes out\n        await match(\n            [first, claims[0], claims[1], claims[2]], page_size=4,\n            limit_claims_per_channel=1, claim_type='stream', order_by=['^height']\n        )\n        # second appears, from replacement queue\n        await match(\n            [second, claims[3], claims[4], claims[5]], page_size=4, page=2,\n            limit_claims_per_channel=1, claim_type='stream', order_by=['^height']\n        )\n        # last is unaffected, as the limit applies per page\n        await match(\n            [claims[6], claims[7], last], page_size=4, page=3,\n            limit_claims_per_channel=1, claim_type='stream', order_by=['^height']\n        )\n        # feature disabled on 0 or negative values\n        for limit in [None, 0, -1]:\n            await match(\n                [first, second] + claims + [last],\n                limit_claims_per_channel=limit, claim_type='stream', order_by=['^height']\n            )\n\n    async def test_claim_type_and_media_type_search(self):\n        # create an invalid/unknown claim\n        address = await self.account.receiving.get_or_create_usable_address()\n        tx = await Transaction.claim_create(\n            'unknown', b'{\"sources\":{\"lbry_sd_hash\":\"\"}}', 1, address, [self.account], self.account)\n        await tx.sign([self.account])\n        await self.broadcast_and_confirm(tx)\n\n        octet = await self.stream_create()\n        video = await self.stream_create('chrome', file_path=self.video_file_name)\n        image = await self.stream_create('blank-image', data=self.image_data, suffix='.png')\n        image_repost = await self.stream_repost(self.get_claim_id(image), 'image-repost')\n        video_repost = await self.stream_repost(self.get_claim_id(video), 'video-repost')\n        collection = await self.collection_create('a-collection', claims=[self.get_claim_id(video)])\n        channel = await self.channel_create()\n        unknown = self.sout(tx)\n\n        # claim_type\n        await self.assertFindsClaims([image, video, octet, unknown], claim_type='stream')\n        await self.assertFindsClaims([channel], claim_type='channel')\n        await self.assertFindsClaims([video_repost, image_repost], claim_type='repost')\n        await self.assertFindsClaims([collection], claim_type='collection')\n\n        # stream_type\n        await self.assertFindsClaims([octet, unknown], stream_types=['binary'])\n        await self.assertFindsClaims([video_repost, video], stream_types=['video'])\n        await self.assertFindsClaims([image_repost, image], stream_types=['image'])\n        await self.assertFindsClaims([video_repost, image_repost, image, video], stream_types=['video', 'image'])\n\n        # media_type\n        await self.assertFindsClaims([octet, unknown], media_types=['application/octet-stream'])\n        await self.assertFindsClaims([video_repost, video], media_types=['video/mp4'])\n        await self.assertFindsClaims([image_repost, image], media_types=['image/png'])\n        await self.assertFindsClaims([video_repost, image_repost, image, video], media_types=['video/mp4', 'image/png'])\n\n        # duration\n        await self.assertFindsClaims([video_repost, video], duration='>14')\n        await self.assertFindsClaims([video_repost, video], duration='<16')\n        await self.assertFindsClaims([video_repost, video], duration=15)\n        await self.assertFindsClaims([], duration='>100')\n        await self.assertFindsClaims([], duration='<14')\n\n    async def test_search_by_text(self):\n        chan1_id = self.get_claim_id(await self.channel_create('@SatoshiNakamoto'))\n        chan2_id = self.get_claim_id(await self.channel_create('@Bitcoin'))\n        chan3_id = self.get_claim_id(await self.channel_create('@IAmSatoshi'))\n\n        claim1 = await self.stream_create(\n            \"the-real-satoshi\", title=\"The Real Satoshi Nakamoto\",\n            description=\"Documentary about the real Satoshi Nakamoto, creator of bitcoin.\",\n            tags=['satoshi nakamoto', 'bitcoin', 'documentary']\n        )\n        claim2 = await self.stream_create(\n            \"about-me\", channel_id=chan1_id, title=\"Satoshi Nakamoto Autobiography\",\n            description=\"I am Satoshi Nakamoto and this is my autobiography.\",\n            tags=['satoshi nakamoto', 'bitcoin', 'documentary', 'autobiography']\n        )\n        claim3 = await self.stream_create(\n            \"history-of-bitcoin\", channel_id=chan2_id, title=\"History of Bitcoin\",\n            description=\"History of bitcoin and its creator Satoshi Nakamoto.\",\n            tags=['satoshi nakamoto', 'bitcoin', 'documentary', 'history']\n        )\n        claim4 = await self.stream_create(\n            \"satoshi-conspiracies\", channel_id=chan3_id, title=\"Satoshi Nakamoto Conspiracies\",\n            description=\"Documentary detailing various conspiracies surrounding Satoshi Nakamoto.\",\n            tags=['conspiracies', 'bitcoin', 'satoshi nakamoto']\n        )\n\n        await self.assertFindsClaims([], text='cheese')\n        await self.assertFindsClaims([claim2], text='autobiography')\n        await self.assertFindsClaims([claim3], text='history')\n        await self.assertFindsClaims([claim4], text='conspiracy')\n        await self.assertFindsClaims([], text='conspiracy+history')\n        await self.assertFindsClaims([claim4, claim3], text='conspiracy|history')\n        await self.assertFindsClaims([claim1, claim4, claim2, claim3], text='documentary', order_by=[])\n        # todo: check why claim1 and claim2 order changed. used to be ...claim1, claim2...\n        await self.assertFindsClaims([claim4, claim2, claim1, claim3], text='satoshi', order_by=[])\n\n        claim2 = await self.stream_update(\n            self.get_claim_id(claim2), clear_tags=True, tags=['cloud'],\n            title=\"Satoshi Nakamoto Nography\",\n            description=\"I am Satoshi Nakamoto and this is my nography.\",\n        )\n        await self.assertFindsClaims([], text='autobiography')\n        await self.assertFindsClaims([claim2], text='cloud')\n\n        await self.stream_abandon(self.get_claim_id(claim2))\n        await self.assertFindsClaims([], text='cloud')\n\n\nclass TransactionCommands(ClaimTestCase):\n\n    async def test_transaction_list(self):\n        channel_id = self.get_claim_id(await self.channel_create())\n        await self.channel_update(channel_id, bid='0.5')\n        await self.channel_abandon(claim_id=channel_id)\n        stream_id = self.get_claim_id(await self.stream_create())\n        await self.stream_update(stream_id, bid='0.5')\n        await self.stream_abandon(claim_id=stream_id)\n\n        r = await self.transaction_list()\n        self.assertEqual(7, len(r))\n        self.assertEqual(stream_id, r[0]['abandon_info'][0]['claim_id'])\n        self.assertEqual(stream_id, r[1]['update_info'][0]['claim_id'])\n        self.assertEqual(stream_id, r[2]['claim_info'][0]['claim_id'])\n        self.assertEqual(channel_id, r[3]['abandon_info'][0]['claim_id'])\n        self.assertEqual(channel_id, r[4]['update_info'][0]['claim_id'])\n        self.assertEqual(channel_id, r[5]['claim_info'][0]['claim_id'])\n\n\nclass TransactionOutputCommands(ClaimTestCase):\n\n    async def test_support_with_comment(self):\n        channel = self.get_claim_id(await self.channel_create('@identity'))\n        stream = self.get_claim_id(await self.stream_create())\n        support = await self.support_create(stream, channel_id=channel, comment=\"nice!\")\n        self.assertEqual(support['outputs'][0]['value']['comment'], \"nice!\")\n        r, = await self.txo_list(type='support')\n        self.assertEqual(r['txid'], support['txid'])\n        self.assertEqual(r['value']['comment'], \"nice!\")\n        await self.support_abandon(txid=support['txid'], nout=0, blocking=True)\n        support = await self.support_create(stream, comment=\"anonymously great!\")\n        self.assertEqual(support['outputs'][0]['value']['comment'], \"anonymously great!\")\n        r, = await self.txo_list(type='support', is_not_spent=True)\n        self.assertEqual(r['txid'], support['txid'])\n        self.assertEqual(r['value']['comment'], \"anonymously great!\")\n\n    async def test_txo_list_resolve_supports(self):\n        channel = self.get_claim_id(await self.channel_create('@identity'))\n        stream = self.get_claim_id(await self.stream_create())\n        support = await self.support_create(stream, channel_id=channel)\n        r, = await self.txo_list(type='support')\n        self.assertEqual(r['txid'], support['txid'])\n        self.assertNotIn('name', r['signing_channel'])\n        r, = await self.txo_list(type='support', resolve=True)\n        self.assertIn('name', r['signing_channel'])\n        self.assertEqual(r['signing_channel']['name'], '@identity')\n\n    async def test_txo_list_by_channel_filtering(self):\n        channel_foo = self.get_claim_id(await self.channel_create('@foo'))\n        channel_bar = self.get_claim_id(await self.channel_create('@bar'))\n        stream_a = self.get_claim_id(await self.stream_create('a', channel_id=channel_foo))\n        stream_b = self.get_claim_id(await self.stream_create('b', channel_id=channel_foo))\n        stream_c = self.get_claim_id(await self.stream_create('c', channel_id=channel_bar))\n        stream_d = self.get_claim_id(await self.stream_create('d'))\n        support_c = await self.support_create(stream_c, '0.3', channel_id=channel_foo)\n        support_d = await self.support_create(stream_d, '0.3', channel_id=channel_bar)\n\n        r = await self.txo_list(type='stream')\n        self.assertEqual({stream_a, stream_b, stream_c, stream_d}, {c['claim_id'] for c in r})\n\n        r = await self.txo_list(type='stream', channel_id=channel_foo)\n        self.assertEqual({stream_a, stream_b}, {c['claim_id'] for c in r})\n\n        r = await self.txo_list(type='support', channel_id=channel_foo)\n        self.assertEqual({support_c['txid']}, {s['txid'] for s in r})\n        r = await self.txo_list(type='support', channel_id=channel_bar)\n        self.assertEqual({support_d['txid']}, {s['txid'] for s in r})\n\n        r = await self.txo_list(type='stream', channel_id=[channel_foo, channel_bar])\n        self.assertEqual({stream_a, stream_b, stream_c}, {c['claim_id'] for c in r})\n\n        r = await self.txo_list(type='stream', not_channel_id=channel_foo)\n        self.assertEqual({stream_c, stream_d}, {c['claim_id'] for c in r})\n\n        r = await self.txo_list(type='stream', not_channel_id=[channel_foo, channel_bar])\n        self.assertEqual({stream_d}, {c['claim_id'] for c in r})\n\n    async def test_txo_list_and_sum_filtering(self):\n        channel_id = self.get_claim_id(await self.channel_create())\n        self.assertEqual('1.0', lbc(await self.txo_sum(type='channel', is_not_spent=True)))\n        await self.channel_update(channel_id, bid='0.5')\n        self.assertEqual('0.5', lbc(await self.txo_sum(type='channel', is_not_spent=True)))\n        self.assertEqual('1.5', lbc(await self.txo_sum(type='channel')))\n\n        stream_id = self.get_claim_id(await self.stream_create(bid='1.3'))\n        self.assertEqual('1.3', lbc(await self.txo_sum(type='stream', is_not_spent=True)))\n        await self.stream_update(stream_id, bid='0.7')\n        self.assertEqual('0.7', lbc(await self.txo_sum(type='stream', is_not_spent=True)))\n        self.assertEqual('2.0', lbc(await self.txo_sum(type='stream')))\n\n        self.assertEqual('1.2', lbc(await self.txo_sum(type=['stream', 'channel'], is_not_spent=True)))\n        self.assertEqual('3.5', lbc(await self.txo_sum(type=['stream', 'channel'])))\n\n        # type filtering\n        r = await self.txo_list(type='channel')\n        self.assertEqual(2, len(r))\n        self.assertEqual('channel', r[0]['value_type'])\n        self.assertFalse(r[0]['is_spent'])\n        self.assertEqual('channel', r[1]['value_type'])\n        self.assertTrue(r[1]['is_spent'])\n\n        r = await self.txo_list(type='stream')\n        self.assertEqual(2, len(r))\n        self.assertEqual('stream', r[0]['value_type'])\n        self.assertFalse(r[0]['is_spent'])\n        self.assertEqual('stream', r[1]['value_type'])\n        self.assertTrue(r[1]['is_spent'])\n\n        r = await self.txo_list(type=['stream', 'channel'])\n        self.assertEqual(4, len(r))\n        self.assertEqual({'stream', 'channel'}, {c['value_type'] for c in r})\n\n        # claim_id filtering\n        r = await self.txo_list(claim_id=stream_id)\n        self.assertEqual(2, len(r))\n        self.assertEqual({stream_id}, {c['claim_id'] for c in r})\n\n        r = await self.txo_list(claim_id=[stream_id, channel_id])\n        self.assertEqual(4, len(r))\n        self.assertEqual({stream_id, channel_id}, {c['claim_id'] for c in r})\n        stream_name, _, channel_name, _ = (c['name'] for c in r)\n\n        r = await self.txo_list(claim_id=['beef'])\n        self.assertEqual(0, len(r))\n\n        # claim_name filtering\n        r = await self.txo_list(name=stream_name)\n        self.assertEqual(2, len(r))\n        self.assertEqual({stream_id}, {c['claim_id'] for c in r})\n\n        r = await self.txo_list(name=[stream_name, channel_name])\n        self.assertEqual(4, len(r))\n        self.assertEqual({stream_id, channel_id}, {c['claim_id'] for c in r})\n\n        r = await self.txo_list(name=['beef'])\n        self.assertEqual(0, len(r))\n\n        r = await self.txo_list()\n        self.assertEqual(9, len(r))\n        await self.stream_abandon(claim_id=stream_id)\n        r = await self.txo_list()\n        self.assertEqual(10, len(r))\n        r = await self.txo_list(claim_id=stream_id)\n        self.assertEqual(2, len(r))\n        self.assertTrue(r[0]['is_spent'])\n        self.assertTrue(r[1]['is_spent'])\n\n    async def test_txo_list_my_input_output_filtering(self):\n        wallet2 = await self.daemon.jsonrpc_wallet_create('wallet2', create_account=True)\n        address2 = await self.daemon.jsonrpc_address_unused(wallet_id=wallet2.id)\n        await self.channel_create('@kept-channel')\n        await self.channel_create('@sent-channel', claim_address=address2)\n        await self.wallet_send('2.9', address2)\n\n        # all txos on second wallet\n        received_payment, received_channel = await self.txo_list(\n            wallet_id=wallet2.id, is_my_input_or_output=True)\n        self.assertEqual('1.0', received_channel['amount'])\n        self.assertFalse(received_channel['is_my_input'])\n        self.assertTrue(received_channel['is_my_output'])\n        self.assertFalse(received_channel['is_internal_transfer'])\n        self.assertEqual('2.9', received_payment['amount'])\n        self.assertFalse(received_payment['is_my_input'])\n        self.assertTrue(received_payment['is_my_output'])\n        self.assertFalse(received_payment['is_internal_transfer'])\n\n        # all txos on default wallet\n        r = await self.txo_list(is_my_input_or_output=True)\n        self.assertEqual(\n            ['2.9', '5.047662', '1.0', '7.947786', '1.0', '8.973893', '10.0'],\n            [t['amount'] for t in r]\n        )\n\n        sent_payment, change3, sent_channel, change2, kept_channel, change1, initial_funds = r\n\n        self.assertTrue(sent_payment['is_my_input'])\n        self.assertFalse(sent_payment['is_my_output'])\n        self.assertFalse(sent_payment['is_internal_transfer'])\n        self.assertTrue(change3['is_my_input'])\n        self.assertTrue(change3['is_my_output'])\n        self.assertTrue(change3['is_internal_transfer'])\n\n        self.assertTrue(sent_channel['is_my_input'])\n        self.assertFalse(sent_channel['is_my_output'])\n        self.assertFalse(sent_channel['is_internal_transfer'])\n        self.assertTrue(change2['is_my_input'])\n        self.assertTrue(change2['is_my_output'])\n        self.assertTrue(change2['is_internal_transfer'])\n\n        self.assertTrue(kept_channel['is_my_input'])\n        self.assertTrue(kept_channel['is_my_output'])\n        self.assertFalse(kept_channel['is_internal_transfer'])\n        self.assertTrue(change1['is_my_input'])\n        self.assertTrue(change1['is_my_output'])\n        self.assertTrue(change1['is_internal_transfer'])\n\n        self.assertFalse(initial_funds['is_my_input'])\n        self.assertTrue(initial_funds['is_my_output'])\n        self.assertFalse(initial_funds['is_internal_transfer'])\n\n        # my stuff and stuff i sent excluding \"change\"\n        r = await self.txo_list(is_my_input_or_output=True, exclude_internal_transfers=True)\n        self.assertEqual([sent_payment, sent_channel, kept_channel, initial_funds], r)\n\n        # my unspent stuff and stuff i sent excluding \"change\"\n        r = await self.txo_list(is_my_input_or_output=True, is_not_spent=True, exclude_internal_transfers=True)\n        self.assertEqual([sent_payment, sent_channel, kept_channel], r)\n\n        # only \"change\"\n        r = await self.txo_list(is_my_input=True, is_my_output=True, type=\"other\")\n        self.assertEqual([change3, change2, change1], r)\n\n        # only unspent \"change\"\n        r = await self.txo_list(is_my_input=True, is_my_output=True, type=\"other\", is_not_spent=True)\n        self.assertEqual([change3], r)\n\n        # only spent \"change\"\n        r = await self.txo_list(is_my_input=True, is_my_output=True, type=\"other\", is_spent=True)\n        self.assertEqual([change2, change1], r)\n\n        # all my unspent stuff\n        r = await self.txo_list(is_my_output=True, is_not_spent=True)\n        self.assertEqual([change3, kept_channel], r)\n\n        # stuff i sent\n        r = await self.txo_list(is_not_my_output=True)\n        self.assertEqual([sent_payment, sent_channel], r)\n\n    async def test_txo_plot(self):\n        day_blocks = int((24 * 60 * 60) / self.ledger.headers.timestamp_average_offset)\n        stream_id = self.get_claim_id(await self.stream_create())\n        await self.support_create(stream_id, '0.3')\n        await self.support_create(stream_id, '0.2')\n        await self.generate(day_blocks // 2)\n        await self.stream_update(stream_id)\n        await self.generate(day_blocks // 2)\n        await self.support_create(stream_id, '0.4')\n        await self.support_create(stream_id, '0.5')\n        await self.stream_update(stream_id)\n        await self.generate(day_blocks // 2)\n        await self.stream_update(stream_id)\n        await self.generate(day_blocks // 2)\n        await self.support_create(stream_id, '0.6')\n\n        plot = await self.txo_plot(type='support')\n        self.assertEqual([\n            {'day': '2016-06-25', 'total': '0.6'},\n        ], plot)\n        plot = await self.txo_plot(type='support', days_back=1)\n        self.assertEqual([\n            {'day': '2016-06-24', 'total': '0.9'},\n            {'day': '2016-06-25', 'total': '0.6'},\n        ], plot)\n        plot = await self.txo_plot(type='support', days_back=2)\n        self.assertEqual([\n            {'day': '2016-06-23', 'total': '0.5'},\n            {'day': '2016-06-24', 'total': '0.9'},\n            {'day': '2016-06-25', 'total': '0.6'},\n        ], plot)\n\n        plot = await self.txo_plot(type='support', start_day='2016-06-23')\n        self.assertEqual([\n            {'day': '2016-06-23', 'total': '0.5'},\n            {'day': '2016-06-24', 'total': '0.9'},\n            {'day': '2016-06-25', 'total': '0.6'},\n        ], plot)\n        plot = await self.txo_plot(type='support', start_day='2016-06-24')\n        self.assertEqual([\n            {'day': '2016-06-24', 'total': '0.9'},\n            {'day': '2016-06-25', 'total': '0.6'},\n        ], plot)\n        plot = await self.txo_plot(type='support', start_day='2016-06-23', end_day='2016-06-24')\n        self.assertEqual([\n            {'day': '2016-06-23', 'total': '0.5'},\n            {'day': '2016-06-24', 'total': '0.9'},\n        ], plot)\n        plot = await self.txo_plot(type='support', start_day='2016-06-23', days_after=1)\n        self.assertEqual([\n            {'day': '2016-06-23', 'total': '0.5'},\n            {'day': '2016-06-24', 'total': '0.9'},\n        ], plot)\n        plot = await self.txo_plot(type='support', start_day='2016-06-23', days_after=2)\n        self.assertEqual([\n            {'day': '2016-06-23', 'total': '0.5'},\n            {'day': '2016-06-24', 'total': '0.9'},\n            {'day': '2016-06-25', 'total': '0.6'},\n        ], plot)\n\n    async def test_txo_spend(self):\n        stream_id = self.get_claim_id(await self.stream_create())\n        for _ in range(10):\n            await self.support_create(stream_id, '0.1')\n        await self.assertBalance(self.account, '7.978478')\n        self.assertEqual('1.0', lbc(await self.txo_sum(type='support', is_not_spent=True)))\n        txs = await self.txo_spend(type='support', batch_size=3, include_full_tx=True)\n        self.assertEqual(4, len(txs))\n        self.assertEqual(3, len(txs[0]['inputs']))\n        self.assertEqual(3, len(txs[1]['inputs']))\n        self.assertEqual(3, len(txs[2]['inputs']))\n        self.assertEqual(1, len(txs[3]['inputs']))\n        self.assertEqual('0.0', lbc(await self.txo_sum(type='support', is_not_spent=True)))\n        await self.assertBalance(self.account, '8.977606')\n\n        await self.support_create(stream_id, '0.1')\n        txs = await self.daemon.jsonrpc_txo_spend(type='support', batch_size=3)\n        self.assertEqual(1, len(txs))\n        self.assertEqual({'txid'}, set(txs[0]))\n\n\nclass ClaimCommands(ClaimTestCase):\n\n    async def test_claim_list_filtering(self):\n        channel_id = self.get_claim_id(await self.channel_create())\n        stream_id = self.get_claim_id(await self.stream_create())\n\n        await self.stream_update(stream_id, title='foo')\n\n        # type filtering\n        r = await self.claim_list(claim_type='channel')\n        self.assertEqual(1, len(r))\n        self.assertEqual('channel', r[0]['value_type'])\n\n        # catch a bug where cli sends is_spent=False by default\n        r = await self.claim_list(claim_type='stream', is_spent=False)\n        self.assertEqual(1, len(r))\n        self.assertEqual('stream', r[0]['value_type'])\n\n        r = await self.claim_list(claim_type=['stream', 'channel'])\n        self.assertEqual(2, len(r))\n        self.assertEqual({'stream', 'channel'}, {c['value_type'] for c in r})\n\n        # claim_id filtering\n        r = await self.claim_list(claim_id=stream_id)\n        self.assertEqual(1, len(r))\n        self.assertEqual({stream_id}, {c['claim_id'] for c in r})\n\n        r = await self.claim_list(claim_id=[stream_id, channel_id])\n        self.assertEqual(2, len(r))\n        self.assertEqual({stream_id, channel_id}, {c['claim_id'] for c in r})\n        stream_name, channel_name = (c['name'] for c in r)\n\n        r = await self.claim_list(claim_id=['beef'])\n        self.assertEqual(0, len(r))\n\n        # claim_name filtering\n        r = await self.claim_list(name=stream_name)\n        self.assertEqual(1, len(r))\n        self.assertEqual({stream_id}, {c['claim_id'] for c in r})\n\n        r = await self.claim_list(name=[stream_name, channel_name])\n        self.assertEqual(2, len(r))\n        self.assertEqual({stream_id, channel_id}, {c['claim_id'] for c in r})\n\n        r = await self.claim_list(name=['beef'])\n        self.assertEqual(0, len(r))\n\n    async def test_claim_stream_channel_list_with_resolve(self):\n        self.assertListEqual([], await self.claim_list(resolve=True))\n\n        await self.channel_create()\n        await self.stream_create()\n\n        r = await self.claim_list()\n        self.assertNotIn('short_url', r[0])\n        self.assertNotIn('short_url', r[1])\n        self.assertNotIn('short_url', (await self.stream_list())[0])\n        self.assertNotIn('short_url', (await self.channel_list())[0])\n\n        r = await self.claim_list(resolve=True)\n        self.assertIn('short_url', r[0])\n        self.assertIn('short_url', r[1])\n        self.assertIn('short_url', (await self.stream_list(resolve=True))[0])\n        self.assertIn('short_url', (await self.channel_list(resolve=True))[0])\n\n        # unconfirmed channel won't resolve\n        channel_tx = await self.daemon.jsonrpc_channel_create('@foo', '1.0')\n        await self.ledger.wait(channel_tx)\n\n        r = await self.claim_list(resolve=True)\n        self.assertEqual('NOT_FOUND', r[0]['meta']['error']['name'])\n        self.assertTrue(r[1]['meta']['is_controlling'])\n        r = await self.channel_list(resolve=True)\n        self.assertEqual('NOT_FOUND', r[0]['meta']['error']['name'])\n        self.assertTrue(r[1]['meta']['is_controlling'])\n\n        # confirm it\n        await self.generate(1)\n        await self.ledger.wait(channel_tx, self.blockchain.block_expected)\n\n        # all channel claims resolve\n        r = await self.claim_list(resolve=True)\n        self.assertTrue(r[0]['meta']['is_controlling'])\n        self.assertTrue(r[1]['meta']['is_controlling'])\n        r = await self.channel_list(resolve=True)\n        self.assertTrue(r[0]['meta']['is_controlling'])\n        self.assertTrue(r[1]['meta']['is_controlling'])\n\n        # unconfirmed stream won't resolve\n        stream_tx = await self.daemon.jsonrpc_stream_create(\n            'foo', '1.0', file_path=self.create_upload_file(data=b'hi')\n        )\n        await self.ledger.wait(stream_tx)\n\n        r = await self.claim_list(resolve=True)\n        self.assertEqual('NOT_FOUND', r[0]['meta']['error']['name'])\n        self.assertTrue(r[1]['meta']['is_controlling'])\n        r = await self.stream_list(resolve=True)\n        self.assertEqual('NOT_FOUND', r[0]['meta']['error']['name'])\n        self.assertTrue(r[1]['meta']['is_controlling'])\n\n        # confirm it\n        await self.generate(1)\n        await self.ledger.wait(stream_tx, self.blockchain.block_expected)\n\n        # all claims resolve\n        r = await self.claim_list(resolve=True)\n        self.assertTrue(r[0]['meta']['is_controlling'])\n        self.assertTrue(r[1]['meta']['is_controlling'])\n        self.assertTrue(r[2]['meta']['is_controlling'])\n        self.assertTrue(r[3]['meta']['is_controlling'])\n        r = await self.stream_list(resolve=True)\n        self.assertTrue(r[0]['meta']['is_controlling'])\n        self.assertTrue(r[1]['meta']['is_controlling'])\n        r = await self.channel_list(resolve=True)\n        self.assertTrue(r[0]['meta']['is_controlling'])\n        self.assertTrue(r[1]['meta']['is_controlling'])\n\n        # check that metadata is transfered\n        self.assertTrue(r[0]['is_my_output'])\n\n    async def assertClaimList(self, claim_ids, **kwargs):\n        self.assertEqual(claim_ids, [c['claim_id'] for c in await self.claim_list(**kwargs)])\n\n    async def test_list_streams_in_channel_and_order_by(self):\n        channel1_id = self.get_claim_id(await self.channel_create('@chan-one'))\n        channel2_id = self.get_claim_id(await self.channel_create('@chan-two'))\n        stream1_id = self.get_claim_id(await self.stream_create('stream-a', bid='0.3', channel_id=channel1_id))\n        stream2_id = self.get_claim_id(await self.stream_create('stream-b', bid='0.9', channel_id=channel1_id))\n        stream3_id = self.get_claim_id(await self.stream_create('stream-c', bid='0.6', channel_id=channel2_id))\n        await self.assertClaimList([stream2_id, stream1_id], channel_id=channel1_id)\n        await self.assertClaimList([stream3_id], channel_id=channel2_id)\n        await self.assertClaimList([stream3_id, stream2_id, stream1_id], channel_id=[channel1_id, channel2_id])\n        await self.assertClaimList([stream1_id, stream2_id, stream3_id], claim_type='stream', order_by='name')\n        await self.assertClaimList([stream1_id, stream3_id, stream2_id], claim_type='stream', order_by='amount')\n        await self.assertClaimList([stream3_id, stream2_id, stream1_id], claim_type='stream', order_by='height')\n\n    async def test_claim_list_with_tips(self):\n        wallet2 = await self.daemon.jsonrpc_wallet_create('wallet2', create_account=True)\n        address2 = await self.daemon.jsonrpc_address_unused(wallet_id=wallet2.id)\n\n        await self.wallet_send('5.0', address2)\n\n        stream1_id = self.get_claim_id(await self.stream_create('one'))\n        stream2_id = self.get_claim_id(await self.stream_create('two'))\n\n        claims = await self.claim_list()\n        self.assertNotIn('received_tips', claims[0])\n        self.assertNotIn('received_tips', claims[1])\n\n        claims = await self.claim_list(include_received_tips=True)\n        self.assertEqual('0.0', claims[0]['received_tips'])\n        self.assertEqual('0.0', claims[1]['received_tips'])\n\n        await self.support_create(stream1_id, '0.7', tip=True)\n        await self.support_create(stream1_id, '0.3', tip=True, wallet_id=wallet2.id)\n        await self.support_create(stream1_id, '0.2', tip=True, wallet_id=wallet2.id)\n        await self.support_create(stream2_id, '0.4', tip=True, wallet_id=wallet2.id)\n        await self.support_create(stream2_id, '0.5', tip=True, wallet_id=wallet2.id)\n        await self.support_create(stream2_id, '0.1', tip=True, wallet_id=wallet2.id)\n\n        claims = await self.claim_list(include_received_tips=True)\n        self.assertEqual('1.0', claims[0]['received_tips'])\n        self.assertEqual('1.2', claims[1]['received_tips'])\n\n        await self.support_abandon(stream1_id)\n        claims = await self.claim_list(include_received_tips=True)\n        self.assertEqual('1.0', claims[0]['received_tips'])\n        self.assertEqual('0.0', claims[1]['received_tips'])\n\n        await self.support_abandon(stream2_id)\n        claims = await self.claim_list(include_received_tips=True)\n        self.assertEqual('0.0', claims[0]['received_tips'])\n        self.assertEqual('0.0', claims[1]['received_tips'])\n\n    async def stream_update_and_wait(self, claim_id, **kwargs):\n        tx = await self.daemon.jsonrpc_stream_update(claim_id, **kwargs)\n        await self.ledger.wait(tx)\n\n    async def test_claim_list_pending_edits_ordering(self):\n        stream5_id = self.get_claim_id(await self.stream_create('five'))\n        stream4_id = self.get_claim_id(await self.stream_create('four'))\n        stream3_id = self.get_claim_id(await self.stream_create('three'))\n        stream2_id = self.get_claim_id(await self.stream_create('two'))\n        stream1_id = self.get_claim_id(await self.stream_create('one'))\n        await self.assertClaimList([stream1_id, stream2_id, stream3_id, stream4_id, stream5_id])\n        await self.stream_update_and_wait(stream4_id, title='foo')\n        await self.assertClaimList([stream4_id, stream1_id, stream2_id, stream3_id, stream5_id])\n        await self.stream_update_and_wait(stream3_id, title='foo')\n        await self.assertClaimList([stream4_id, stream3_id, stream1_id, stream2_id, stream5_id])\n\n\nclass ChannelCommands(CommandTestCase):\n\n    async def test_create_channel_names(self):\n        # claim new name\n        await self.channel_create('@foo')\n        self.assertItemCount(await self.daemon.jsonrpc_channel_list(), 1)\n        await self.assertBalance(self.account, '8.991893')\n\n        # fail to claim duplicate\n        with self.assertRaisesRegex(Exception, \"You already have a channel under the name '@foo'.\"):\n            await self.channel_create('@foo')\n\n        # fail to claim invalid name\n        with self.assertRaisesRegex(Exception, \"Channel names must start with '@' symbol.\"):\n            await self.channel_create('foo')\n\n        # nothing's changed after failed attempts\n        self.assertItemCount(await self.daemon.jsonrpc_channel_list(), 1)\n        await self.assertBalance(self.account, '8.991893')\n\n        # succeed overriding duplicate restriction\n        await self.channel_create('@foo', allow_duplicate_name=True)\n        self.assertItemCount(await self.daemon.jsonrpc_channel_list(), 2)\n        await self.assertBalance(self.account, '7.983786')\n\n    async def test_channel_bids(self):\n        # enough funds\n        tx = await self.channel_create('@foo', '5.0')\n        claim_id = self.get_claim_id(tx)\n        self.assertItemCount(await self.daemon.jsonrpc_channel_list(), 1)\n        await self.assertBalance(self.account, '4.991893')\n\n        # bid preserved on update\n        tx = await self.channel_update(claim_id)\n        self.assertEqual(tx['outputs'][0]['amount'], '5.0')\n\n        # bid changed on update\n        tx = await self.channel_update(claim_id, bid='4.0')\n        self.assertEqual(tx['outputs'][0]['amount'], '4.0')\n\n        await self.assertBalance(self.account, '5.991503')\n\n        # not enough funds\n        with self.assertRaisesRegex(\n                InsufficientFundsError, \"Not enough funds to cover this transaction.\"):\n            await self.channel_create('@foo2', '9.0')\n        self.assertItemCount(await self.daemon.jsonrpc_channel_list(), 1)\n        await self.assertBalance(self.account, '5.991503')\n\n        # spend exactly amount available, no change\n        tx = await self.channel_create('@foo3', '5.981322')\n        await self.assertBalance(self.account, '0.0')\n        self.assertEqual(len(tx['outputs']), 1)  # no change\n        self.assertItemCount(await self.daemon.jsonrpc_channel_list(), 2)\n\n    async def test_setting_channel_fields(self):\n        values = {\n            'title': \"Cool Channel\",\n            'description': \"Best channel on LBRY.\",\n            'thumbnail_url': \"https://co.ol/thumbnail.png\",\n            'tags': [\"cool\", \"awesome\"],\n            'languages': [\"en-US\"],\n            'locations': ['US::Manchester'],\n            'email': \"human@email.com\",\n            'website_url': \"https://co.ol\",\n            'cover_url': \"https://co.ol/cover.png\",\n            'featured': ['cafe']\n        }\n        fixed_values = values.copy()\n        fixed_values['thumbnail'] = {'url': fixed_values.pop('thumbnail_url')}\n        fixed_values['locations'] = [{'country': 'US', 'city': 'Manchester'}]\n        fixed_values['cover'] = {'url': fixed_values.pop('cover_url')}\n\n        # create new channel with all fields set\n        tx = await self.out(self.channel_create('@bigchannel', **values))\n        channel = tx['outputs'][0]['value']\n        self.assertEqual(channel, {\n            'public_key': channel['public_key'],\n            'public_key_id': channel['public_key_id'],\n            **fixed_values\n        })\n\n        # create channel with nothing set\n        tx = await self.out(self.channel_create('@lightchannel'))\n        channel = tx['outputs'][0]['value']\n        self.assertEqual(\n            channel, {'public_key': channel['public_key'], 'public_key_id': channel['public_key_id']})\n\n        # create channel with just a featured claim\n        tx = await self.out(self.channel_create('@featurechannel', featured='beef'))\n        txo = tx['outputs'][0]\n        claim_id, channel = txo['claim_id'], txo['value']\n        fixed_values['public_key'] = channel['public_key']\n        fixed_values['public_key_id'] = channel['public_key_id']\n        self.assertEqual(channel, {\n            'public_key': fixed_values['public_key'],\n            'public_key_id': fixed_values['public_key_id'],\n            'featured': ['beef']\n        })\n\n        # update channel \"@featurechannel\" setting all fields\n        tx = await self.out(self.channel_update(claim_id, **values))\n        channel = tx['outputs'][0]['value']\n        fixed_values['featured'].insert(0, 'beef')  # existing featured claim\n        self.assertEqual(channel, fixed_values)\n\n        # clearing and settings featured content\n        tx = await self.out(self.channel_update(claim_id, featured='beefcafe', clear_featured=True))\n        channel = tx['outputs'][0]['value']\n        fixed_values['featured'] = ['beefcafe']\n        self.assertEqual(channel, fixed_values)\n\n        # reset signing key\n        tx = await self.out(self.channel_update(claim_id, new_signing_key=True))\n        channel = tx['outputs'][0]['value']\n        self.assertNotEqual(channel['public_key'], fixed_values['public_key'])\n\n        # replace mode (clears everything except public_key)\n        tx = await self.out(self.channel_update(claim_id, replace=True, title='foo', email='new@email.com'))\n        self.assertEqual(tx['outputs'][0]['value'], {\n            'public_key': channel['public_key'],\n            'public_key_id': channel['public_key_id'],\n            'title': 'foo', 'email': 'new@email.com'}\n        )\n\n        # move channel to another account\n        new_account = await self.out(self.daemon.jsonrpc_account_create('second account'))\n        account2_id, account2 = new_account['id'], self.wallet.get_account_or_error(new_account['id'])\n\n        # before moving\n        self.assertItemCount(await self.daemon.jsonrpc_channel_list(), 3)\n        self.assertItemCount(await self.daemon.jsonrpc_channel_list(account_id=account2_id), 0)\n\n        other_address = await account2.receiving.get_or_create_usable_address()\n        tx = await self.out(self.channel_update(claim_id, claim_address=other_address))\n\n        # after moving\n        self.assertItemCount(await self.daemon.jsonrpc_channel_list(), 3)\n        self.assertItemCount(await self.daemon.jsonrpc_channel_list(account_id=self.account.id), 2)\n        self.assertItemCount(await self.daemon.jsonrpc_channel_list(account_id=account2_id), 1)\n\n    async def test_sign_hex_encoded_data(self):\n        data_to_sign = \"CAFEBABE\"\n        # claim new name\n        await self.channel_create('@someotherchan')\n        channel_tx = await self.daemon.jsonrpc_channel_create('@signer', '0.1', blocking=True)\n        await self.confirm_tx(channel_tx.id)\n        channel = channel_tx.outputs[0]\n        signature1 = await self.out(self.daemon.jsonrpc_channel_sign(channel_name='@signer', hexdata=data_to_sign))\n        signature2 = await self.out(self.daemon.jsonrpc_channel_sign(channel_id=channel.claim_id, hexdata=data_to_sign))\n        signature3 = await self.out(self.daemon.jsonrpc_channel_sign(channel_id=channel.claim_id, hexdata=data_to_sign, salt='beef'))\n        signature4 = await self.out(self.daemon.jsonrpc_channel_sign(channel_id=channel.claim_id, hexdata=data_to_sign, salt='beef'))\n        self.assertNotEqual(signature2, signature3)\n        self.assertEqual(signature3, signature4)\n        self.assertTrue(verify(channel, unhexlify(data_to_sign), signature1))\n        self.assertTrue(verify(channel, unhexlify(data_to_sign), signature2))\n        self.assertTrue(verify(channel, unhexlify(data_to_sign), signature3))\n        signature3 = await self.out(self.daemon.jsonrpc_channel_sign(channel_id=channel.claim_id, hexdata=99))\n        self.assertTrue(verify(channel, unhexlify('99'), signature3))\n\n    async def test_channel_export_import_before_sending_channel(self):\n        # export\n        tx = await self.channel_create('@foo', '1.0')\n        claim_id = self.get_claim_id(tx)\n        channel_private_key = (await self.account.get_channels())[0].private_key\n        exported_data = await self.out(self.daemon.jsonrpc_channel_export(claim_id))\n\n        # import\n        daemon2 = await self.add_daemon()\n        self.assertItemCount(await daemon2.jsonrpc_channel_list(), 0)\n        await daemon2.jsonrpc_channel_import(exported_data)\n        channels = (await daemon2.jsonrpc_channel_list())['items']\n        self.assertEqual(1, len(channels))\n        self.assertEqual(channel_private_key.private_key_bytes, channels[0].private_key.private_key_bytes)\n\n        # second wallet can't update until channel is sent to it\n        with self.assertRaisesRegex(AssertionError, 'Cannot find private key for signing output.'):\n            await daemon2.jsonrpc_channel_update(claim_id, bid='0.5')\n\n        # now send the channel as well\n        await self.channel_update(claim_id, claim_address=await daemon2.jsonrpc_address_unused())\n\n        # second wallet should be able to update now\n        await daemon2.jsonrpc_channel_update(claim_id, bid='0.5')\n\n    async def test_channel_update_across_accounts(self):\n        account2 = await self.daemon.jsonrpc_account_create('second account')\n        channel = await self.out(self.channel_create('@spam', '1.0', account_id=account2.id))\n        # channel not in account1\n        with self.assertRaisesRegex(Exception, \"Can't find the channel\"):\n            await self.channel_update(self.get_claim_id(channel), bid='2.0', account_id=self.account.id)\n        # channel is in account2\n        await self.channel_update(self.get_claim_id(channel), bid='2.0', account_id=account2.id)\n        result = (await self.out(self.daemon.jsonrpc_channel_list()))['items']\n        self.assertEqual(result[0]['amount'], '2.0')\n        # check all accounts for channel\n        await self.channel_update(self.get_claim_id(channel), bid='3.0')\n        result = (await self.out(self.daemon.jsonrpc_channel_list()))['items']\n        self.assertEqual(result[0]['amount'], '3.0')\n        await self.channel_abandon(self.get_claim_id(channel))\n\n    async def test_tag_normalization(self):\n        tx1 = await self.channel_create('@abc', '1.0', tags=['aBc', ' ABC ', 'xYZ ', 'xyz'])\n        claim_id = self.get_claim_id(tx1)\n        self.assertCountEqual(tx1['outputs'][0]['value']['tags'], ['abc', 'xyz'])\n\n        tx2 = await self.channel_update(claim_id, tags=[' pqr', 'PQr '])\n        self.assertCountEqual(tx2['outputs'][0]['value']['tags'], ['abc', 'xyz', 'pqr'])\n\n        tx3 = await self.channel_update(claim_id, tags=' pqr')\n        self.assertCountEqual(tx3['outputs'][0]['value']['tags'], ['abc', 'xyz', 'pqr'])\n\n        tx4 = await self.channel_update(claim_id, tags=[' pqr', 'PQr '], clear_tags=True)\n        self.assertEqual(tx4['outputs'][0]['value']['tags'], ['pqr'])\n\n\nclass StreamCommands(ClaimTestCase):\n\n    async def test_create_stream_names(self):\n        # claim new name\n        await self.stream_create('foo')\n        self.assertItemCount(await self.daemon.jsonrpc_claim_list(), 1)\n        await self.assertBalance(self.account, '8.993893')\n\n        # fail to claim duplicate\n        with self.assertRaisesRegex(\n                Exception, \"You already have a stream claim published under the name 'foo'.\"):\n            await self.stream_create('foo')\n\n        # fail claim starting with @\n        with self.assertRaisesRegex(\n                Exception, \"Stream names cannot start with '@' symbol.\"):\n            await self.stream_create('@foo')\n\n        self.assertItemCount(await self.daemon.jsonrpc_claim_list(), 1)\n        await self.assertBalance(self.account, '8.993893')\n\n        # succeed overriding duplicate restriction\n        await self.stream_create('foo', allow_duplicate_name=True)\n        self.assertItemCount(await self.daemon.jsonrpc_claim_list(), 2)\n        await self.assertBalance(self.account, '7.987786')\n\n    async def test_stream_bids(self):\n        # enough funds\n        tx = await self.stream_create('foo', '2.0')\n        claim_id = self.get_claim_id(tx)\n        self.assertItemCount(await self.daemon.jsonrpc_claim_list(), 1)\n        await self.assertBalance(self.account, '7.993893')\n\n        # bid preserved on update\n        tx = await self.stream_update(claim_id)\n        self.assertEqual(tx['outputs'][0]['amount'], '2.0')\n\n        # bid changed on update\n        tx = await self.stream_update(claim_id, bid='3.0')\n        self.assertEqual(tx['outputs'][0]['amount'], '3.0')\n\n        await self.assertBalance(self.account, '6.993319')\n\n        # not enough funds\n        with self.assertRaisesRegex(\n                InsufficientFundsError, \"Not enough funds to cover this transaction.\"):\n            await self.stream_create('foo2', '9.0')\n        self.assertItemCount(await self.daemon.jsonrpc_claim_list(), 1)\n        await self.assertBalance(self.account, '6.993319')\n\n        # spend exactly amount available, no change\n        tx = await self.stream_create('foo3', '6.98523')\n        await self.assertBalance(self.account, '0.0')\n        self.assertEqual(len(tx['outputs']), 1)  # no change\n        self.assertItemCount(await self.daemon.jsonrpc_claim_list(), 2)\n\n    async def test_stream_update_and_abandon_across_accounts(self):\n        account2 = await self.daemon.jsonrpc_account_create('second account')\n        stream = await self.out(self.stream_create('spam', '1.0', account_id=account2.id))\n        # stream not in account1\n        with self.assertRaisesRegex(Exception, \"Can't find the stream\"):\n            await self.stream_update(self.get_claim_id(stream), bid='2.0', account_id=self.account.id)\n        # stream is in account2\n        await self.stream_update(self.get_claim_id(stream), bid='2.0', account_id=account2.id)\n        result = (await self.out(self.daemon.jsonrpc_stream_list()))['items']\n        self.assertEqual(result[0]['amount'], '2.0')\n        # check all accounts for stream\n        await self.stream_update(self.get_claim_id(stream), bid='3.0')\n        result = (await self.out(self.daemon.jsonrpc_stream_list()))['items']\n        self.assertEqual(result[0]['amount'], '3.0')\n        await self.stream_abandon(self.get_claim_id(stream))\n\n    async def test_publishing_checks_all_accounts_for_channel(self):\n        account1_id, account1 = self.account.id, self.account\n        new_account = await self.out(self.daemon.jsonrpc_account_create('second account'))\n        account2_id, account2 = new_account['id'], self.wallet.get_account_or_error(new_account['id'])\n\n        await self.out(self.channel_create('@spam', '1.0'))\n        self.assertEqual('8.989893', (await self.daemon.jsonrpc_account_balance())['available'])\n\n        result = await self.out(self.daemon.jsonrpc_account_send(\n            '5.0', await self.daemon.jsonrpc_address_unused(account2_id), blocking=True\n        ))\n        await self.confirm_tx(result['txid'])\n\n        self.assertEqual('3.989769', (await self.daemon.jsonrpc_account_balance())['available'])\n        self.assertEqual('5.0', (await self.daemon.jsonrpc_account_balance(account2_id))['available'])\n\n        baz_tx = await self.out(self.channel_create('@baz', '1.0', account_id=account2_id))\n        baz_id = self.get_claim_id(baz_tx)\n\n        channels = await self.out(self.daemon.jsonrpc_channel_list(account1_id))\n        self.assertItemCount(channels, 1)\n        self.assertEqual(channels['items'][0]['name'], '@spam')\n        self.assertEqual(channels, await self.out(self.daemon.jsonrpc_channel_list(account1_id)))\n\n        channels = await self.out(self.daemon.jsonrpc_channel_list(account2_id))\n        self.assertItemCount(channels, 1)\n        self.assertEqual(channels['items'][0]['name'], '@baz')\n\n        channels = await self.out(self.daemon.jsonrpc_channel_list())\n        self.assertItemCount(channels, 2)\n        self.assertEqual(channels['items'][0]['name'], '@baz')\n        self.assertEqual(channels['items'][1]['name'], '@spam')\n\n        # defaults to using all accounts to lookup channel\n        await self.stream_create('hovercraft1', '0.1', channel_id=baz_id)\n        self.assertEqual((await self.claim_search(name='hovercraft1'))[0]['signing_channel']['name'], '@baz')\n        # lookup by channel_name in all accounts\n        await self.stream_create('hovercraft2', '0.1', channel_name='@baz')\n        self.assertEqual((await self.claim_search(name='hovercraft2'))[0]['signing_channel']['name'], '@baz')\n        # uses only the specific accounts which contains the channel\n        await self.stream_create('hovercraft3', '0.1', channel_id=baz_id, channel_account_id=[account2_id])\n        self.assertEqual((await self.claim_search(name='hovercraft3'))[0]['signing_channel']['name'], '@baz')\n        # lookup by channel_name in specific account\n        await self.stream_create('hovercraft4', '0.1', channel_name='@baz', channel_account_id=[account2_id])\n        self.assertEqual((await self.claim_search(name='hovercraft4'))[0]['signing_channel']['name'], '@baz')\n        # fails when specifying account which does not contain channel\n        with self.assertRaisesRegex(ValueError, \"Couldn't find channel with channel_id\"):\n            await self.stream_create(\n                'hovercraft5', '0.1', channel_id=baz_id, channel_account_id=[account1_id]\n            )\n        # fail with channel_name\n        with self.assertRaisesRegex(ValueError, \"Couldn't find channel with channel_name '@baz'\"):\n            await self.stream_create(\n                'hovercraft5', '0.1', channel_name='@baz', channel_account_id=[account1_id]\n            )\n\n        # signing with channel works even if channel and certificate are in different accounts\n        await self.channel_update(\n            baz_id, account_id=account2_id,\n            claim_address=await self.daemon.jsonrpc_address_unused(account1_id)\n        )\n        await self.stream_create(\n            'hovercraft5', '0.1', channel_id=baz_id\n        )\n\n    async def test_preview_works_with_signed_streams(self):\n        await self.channel_create('@spam', '1.0')\n        signed = await self.stream_create('bar', '1.0', channel_name='@spam', preview=True, confirm=False)\n        self.assertTrue(signed['outputs'][0]['is_channel_signature_valid'])\n\n    async def test_repost(self):\n        tx = await self.channel_create('@goodies', '1.0')\n        goodies_claim_id = self.get_claim_id(tx)\n        tx = await self.channel_create('@spam', '1.0')\n        spam_claim_id = self.get_claim_id(tx)\n\n        tx = await self.stream_create('newstuff', '1.1', channel_name='@goodies', tags=['foo', 'gaming'])\n        claim_id = self.get_claim_id(tx)\n\n        self.assertEqual((await self.claim_search(name='newstuff'))[0]['meta']['reposted'], 0)\n        self.assertItemCount(await self.daemon.jsonrpc_txo_list(reposted_claim_id=claim_id), 0)\n        self.assertItemCount(await self.daemon.jsonrpc_txo_list(type='repost'), 0)\n\n        tx = await self.stream_repost(\n            claim_id, 'newstuff-again', '1.1', channel_name='@spam',\n            title=\"repost title\", description=\"repost desc\", tags=[\"tag1\", \"tag2\"]\n        )\n        repost_id = self.get_claim_id(tx)\n\n        # test inflating reposted channels works\n        repost_url = f'newstuff-again:{repost_id}'\n        self.ledger._tx_cache.clear()\n        repost_resolve = await self.out(self.daemon.jsonrpc_resolve(repost_url))\n        repost = repost_resolve[repost_url]\n        self.assertEqual(goodies_claim_id, repost['reposted_claim']['signing_channel']['claim_id'])\n        self.assertEqual(\"repost title\", repost[\"value\"][\"title\"])\n        self.assertEqual(\"repost desc\", repost[\"value\"][\"description\"])\n        self.assertEqual([\"tag1\", \"tag2\"], repost[\"value\"][\"tags\"])\n\n        await self.stream_update(\n            repost_id, title=\"title 2\", description=\"desc 2\", tags=[\"tag3\"]\n        )\n        repost_resolve = await self.out(self.daemon.jsonrpc_resolve(repost_url))\n        repost = repost_resolve[repost_url]\n        self.assertEqual(goodies_claim_id, repost['reposted_claim']['signing_channel']['claim_id'])\n        self.assertEqual(\"title 2\", repost[\"value\"][\"title\"])\n        self.assertEqual(\"desc 2\", repost[\"value\"][\"description\"])\n        self.assertEqual([\"tag1\", \"tag2\", \"tag3\"], repost[\"value\"][\"tags\"])\n\n        self.assertItemCount(await self.daemon.jsonrpc_claim_list(claim_type='repost'), 1)\n        self.assertEqual((await self.claim_search(name='newstuff'))[0]['meta']['reposted'], 1)\n        self.assertEqual((await self.claim_search(reposted_claim_id=claim_id))[0]['claim_id'], repost_id)\n        self.assertEqual((await self.txo_list(reposted_claim_id=claim_id))[0]['claim_id'], repost_id)\n        self.assertEqual((await self.txo_list(type='repost'))[0]['claim_id'], repost_id)\n\n        # tags are inherited (non-common / indexed tags)\n        self.assertItemCount(await self.daemon.jsonrpc_claim_search(any_tags=['foo'], claim_type=['stream', 'repost']), 2)\n        self.assertItemCount(await self.daemon.jsonrpc_claim_search(all_tags=['foo'], claim_type=['stream', 'repost']), 2)\n        self.assertItemCount(await self.daemon.jsonrpc_claim_search(not_tags=['foo'], claim_type=['stream', 'repost']), 0)\n        # \"common\" / indexed tags work too\n        self.assertItemCount(await self.daemon.jsonrpc_claim_search(any_tags=['gaming'], claim_type=['stream', 'repost']), 2)\n        self.assertItemCount(await self.daemon.jsonrpc_claim_search(all_tags=['gaming'], claim_type=['stream', 'repost']), 2)\n        self.assertItemCount(await self.daemon.jsonrpc_claim_search(not_tags=['gaming'], claim_type=['stream', 'repost']), 0)\n\n        await self.channel_create('@reposting-goodies', '1.0')\n        await self.stream_repost(claim_id, 'repost-on-channel', '1.1', channel_name='@reposting-goodies')\n        self.assertItemCount(await self.daemon.jsonrpc_claim_list(claim_type='repost'), 2)\n        self.assertItemCount(await self.daemon.jsonrpc_claim_search(reposted_claim_id=claim_id), 2)\n        self.assertEqual((await self.claim_search(name='newstuff'))[0]['meta']['reposted'], 2)\n\n        search_results = await self.claim_search(reposted='>=2')\n        self.assertEqual(len(search_results), 1)\n        self.assertEqual(search_results[0]['name'], 'newstuff')\n\n        search_results = await self.claim_search(name='repost-on-channel')\n        self.assertEqual(len(search_results), 1)\n        search = search_results[0]\n        self.assertEqual(search['name'], 'repost-on-channel')\n        self.assertEqual(search['signing_channel']['name'], '@reposting-goodies')\n        self.assertEqual(search['reposted_claim']['name'], 'newstuff')\n        self.assertEqual(search['reposted_claim']['meta']['reposted'], 2)\n        self.assertEqual(search['reposted_claim']['signing_channel']['name'], '@goodies')\n\n        resolved = await self.out(\n            self.daemon.jsonrpc_resolve(['@reposting-goodies/repost-on-channel', 'newstuff-again'])\n        )\n        self.assertEqual(resolved['@reposting-goodies/repost-on-channel'], search)\n        self.assertEqual(resolved['newstuff-again']['reposted_claim']['name'], 'newstuff')\n\n        await self.stream_update(repost_id, bid='0.42')\n        searched_repost = (await self.claim_search(claim_id=repost_id))[0]\n        self.assertEqual(searched_repost['amount'], '0.42')\n        self.assertEqual(searched_repost['signing_channel']['claim_id'], spam_claim_id)\n\n    async def test_filtering_channels_for_removing_content(self):\n        some_channel_id = self.get_claim_id(await self.channel_create('@some_channel', '0.1'))\n        await self.stream_create('good_content', '0.1', channel_name='@some_channel', tags=['good'])\n        bad_content_id = self.get_claim_id(\n            await self.stream_create('bad_content', '0.1', channel_name='@some_channel', tags=['bad'])\n        )\n        filtering_channel_id = self.get_claim_id(\n            await self.channel_create('@filtering', '0.1')\n        )\n        self.conductor.spv_node.server.db.filtering_channel_hashes.add(bytes.fromhex(filtering_channel_id))\n        self.conductor.spv_node.es_writer.db.filtering_channel_hashes.add(bytes.fromhex(filtering_channel_id))\n\n        self.assertEqual(0, len(self.conductor.spv_node.es_writer.db.filtered_streams))\n        await self.stream_repost(bad_content_id, 'filter1', '0.1', channel_name='@filtering')\n        self.assertEqual(1, len(self.conductor.spv_node.es_writer.db.filtered_streams))\n\n        self.assertEqual('0.1', (await self.out(self.daemon.jsonrpc_resolve('bad_content')))['bad_content']['amount'])\n        # search for filtered content directly\n        result = await self.out(self.daemon.jsonrpc_claim_search(name='bad_content'))\n        blocked = result['blocked']\n        self.assertEqual([], result['items'])\n        self.assertEqual(1, blocked['total'])\n        self.assertEqual(1, len(blocked['channels']))\n        self.assertEqual(1, blocked['channels'][0]['blocked'])\n        self.assertTrue(blocked['channels'][0]['channel']['short_url'].startswith('lbry://@filtering#'))\n\n        # same search, but details omitted by 'no_totals'\n        last_result = result\n        result = await self.out(self.daemon.jsonrpc_claim_search(name='bad_content', no_totals=True))\n        self.assertEqual(result['items'], last_result['items'])\n\n        # search inside channel containing filtered content\n        result = await self.out(self.daemon.jsonrpc_claim_search(channel='@some_channel'))\n        filtered = result['blocked']\n        self.assertEqual(1, len(result['items']))\n        self.assertEqual(1, filtered['total'])\n        self.assertEqual(1, len(filtered['channels']))\n        self.assertEqual(1, filtered['channels'][0]['blocked'])\n        self.assertTrue(filtered['channels'][0]['channel']['short_url'].startswith('lbry://@filtering#'))\n\n        # same search, but details omitted by 'no_totals'\n        last_result = result\n        result = await self.out(self.daemon.jsonrpc_claim_search(channel='@some_channel', no_totals=True))\n        self.assertEqual(result['items'], last_result['items'])\n\n        # content was filtered by not_tag before censoring\n        result = await self.out(self.daemon.jsonrpc_claim_search(channel='@some_channel', not_tags=[\"good\", \"bad\"]))\n        self.assertEqual(0, len(result['items']))\n        self.assertEqual({\"channels\": [], \"total\": 0}, result['blocked'])\n\n        # filtered content can still be resolved\n        result = await self.resolve('lbry://@some_channel/bad_content')\n        self.assertEqual(bad_content_id, result['claim_id'])\n\n        blocking_channel_id = self.get_claim_id(\n            await self.channel_create('@blocking', '0.1')\n        )\n        # test setting from env vars and starting from scratch\n        await self.conductor.spv_node.stop(False)\n        await self.conductor.spv_node.start(self.conductor.lbcwallet_node,\n                                            extraconf={'blocking_channel_ids': [blocking_channel_id],\n                                                       'filtering_channel_ids': [filtering_channel_id]})\n        await self.daemon.wallet_manager.reset()\n\n        self.assertEqual(0, len(self.conductor.spv_node.es_writer.db.blocked_streams))\n        await self.stream_repost(bad_content_id, 'block1', '0.1', channel_name='@blocking')\n        self.assertEqual(1, len(self.conductor.spv_node.es_writer.db.blocked_streams))\n\n        # blocked content is not resolveable\n        error = (await self.resolve('lbry://@some_channel/bad_content'))['error']\n        self.assertEqual(error['name'], 'BLOCKED')\n        self.assertTrue(error['text'].startswith(f\"Resolve of 'lbry://@some_channel#{some_channel_id[:1]}/bad_content#{bad_content_id[:1]}' was blocked\"))\n        self.assertTrue(error['censor']['short_url'].startswith('lbry://@blocking#'))\n\n        # local claim list still finds local reposted content that's blocked\n        claims = await self.claim_list(reposted_claim_id=bad_content_id)\n        self.assertEqual(claims[0]['name'], 'block1')\n        self.assertEqual(claims[0]['value']['claim_id'], bad_content_id)\n        self.assertEqual(claims[1]['name'], 'filter1')\n        self.assertEqual(claims[1]['value']['claim_id'], bad_content_id)\n\n        # a filtered/blocked channel impacts all content inside it\n        bad_channel_id = self.get_claim_id(\n            await self.channel_create('@bad_channel', '0.1', tags=['bad-stuff'])\n        )\n        worse_content_id = self.get_claim_id(\n            await self.stream_create('worse_content', '0.1', channel_name='@bad_channel', tags=['bad-stuff'])\n        )\n\n        # check search before filtering channel\n        result = await self.out(self.daemon.jsonrpc_claim_search(any_tags=['bad-stuff'], order_by=['height']))\n        self.assertEqual(2, result['total_items'])\n        self.assertEqual('worse_content', result['items'][0]['name'])\n        self.assertEqual('@bad_channel', result['items'][1]['name'])\n\n        # filter channel out\n        self.assertEqual(0, len(self.conductor.spv_node.server.db.filtered_channels))\n        await self.stream_repost(bad_channel_id, 'filter2', '0.1', channel_name='@filtering')\n        self.assertEqual(1, len(self.conductor.spv_node.server.db.filtered_channels))\n\n        # same claim search as previous now returns 0 results\n        result = await self.out(self.daemon.jsonrpc_claim_search(any_tags=['bad-stuff'], order_by=['height']))\n        filtered = result['blocked']\n        self.assertEqual(0, len(result['items']))\n        self.assertEqual(3, filtered['total'])\n        self.assertEqual(1, len(filtered['channels']))\n        self.assertEqual(3, filtered['channels'][0]['blocked'])\n        self.assertTrue(filtered['channels'][0]['channel']['short_url'].startswith('lbry://@filtering#'))\n\n        # same search, but details omitted by 'no_totals'\n        last_result = result\n        result = await self.out(\n            self.daemon.jsonrpc_claim_search(any_tags=['bad-stuff'], order_by=['height'], no_totals=True)\n        )\n        self.assertEqual(result['items'], last_result['items'])\n\n        # filtered channel should still resolve\n        result = await self.resolve('lbry://@bad_channel')\n        self.assertEqual(bad_channel_id, result['claim_id'])\n        result = await self.resolve('lbry://@bad_channel/worse_content')\n        self.assertEqual(worse_content_id, result['claim_id'])\n\n        # block channel\n        self.assertEqual(0, len(self.conductor.spv_node.server.db.blocked_channels))\n        await self.stream_repost(bad_channel_id, 'block2', '0.1', channel_name='@blocking')\n        self.assertEqual(1, len(self.conductor.spv_node.server.db.blocked_channels))\n\n        # channel, claim in channel or claim individually no longer resolve\n        self.assertEqual((await self.resolve('lbry://@bad_channel'))['error']['name'], 'BLOCKED')\n        self.assertEqual((await self.resolve('lbry://worse_content'))['error']['name'], 'BLOCKED')\n        self.assertEqual((await self.resolve('lbry://@bad_channel/worse_content'))['error']['name'], 'BLOCKED')\n\n        await self.stream_update(worse_content_id, channel_name='@bad_channel', tags=['bad-stuff'])\n        self.assertEqual((await self.resolve('lbry://@bad_channel'))['error']['name'], 'BLOCKED')\n        self.assertEqual((await self.resolve('lbry://worse_content'))['error']['name'], 'BLOCKED')\n        self.assertEqual((await self.resolve('lbry://@bad_channel/worse_content'))['error']['name'], 'BLOCKED')\n\n    async def test_publish_updates_file_list(self):\n        tx = await self.stream_create(title='created')\n        txo = tx['outputs'][0]\n        claim_id, expected = txo['claim_id'], txo['value']\n        files = await self.file_list()\n        self.assertEqual(1, len(files))\n        self.assertEqual(tx['txid'], files[0]['txid'])\n        self.assertEqual(expected, files[0]['metadata'])\n\n        # update with metadata-only changes\n        tx = await self.stream_update(claim_id, title='update 1')\n        files = await self.file_list()\n        expected['title'] = 'update 1'\n        self.assertEqual(1, len(files))\n        self.assertEqual(tx['txid'], files[0]['txid'])\n        self.assertEqual(expected, files[0]['metadata'])\n\n        # update with new data\n        tx = await self.stream_update(claim_id, title='update 2', data=b'updated data')\n        expected = tx['outputs'][0]['value']\n        files = await self.file_list()\n        self.assertEqual(1, len(files))\n        self.assertEqual(tx['txid'], files[0]['txid'])\n        self.assertEqual(expected, files[0]['metadata'])\n\n    async def test_setting_stream_fields(self):\n        values = {\n            'title': \"Cool Content\",\n            'description': \"Best content on LBRY.\",\n            'thumbnail_url': \"https://co.ol/thumbnail.png\",\n            'tags': [\"cool\", \"awesome\"],\n            'languages': [\"en\"],\n            'locations': ['US:NH:Manchester:03101:42.990605:-71.460989'],\n\n            'author': \"Jules Verne\",\n            'license': 'Public Domain',\n            'license_url': \"https://co.ol/license\",\n            'release_time': 123456,\n\n            'fee_currency': 'usd',\n            'fee_amount': '2.99',\n            'fee_address': 'mmCsWAiXMUVecFQ3fVzUwvpT9XFMXno2Ca',\n        }\n        fixed_values = values.copy()\n        fixed_values['locations'] = [{\n            'country': 'US',\n            'state': 'NH',\n            'city': 'Manchester',\n            'code': '03101',\n            'latitude': '42.990605',\n            'longitude': '-71.460989'\n        }]\n        fixed_values['thumbnail'] = {'url': fixed_values.pop('thumbnail_url')}\n        fixed_values['release_time'] = str(values['release_time'])\n        fixed_values['stream_type'] = 'binary'\n        fixed_values['source'] = {\n            'hash': '56bf5dbae43f77a63d075b0f2ae9c7c3e3098db93779c7f9840da0f4db9c2f8c8454f4edd1373e2b64ee2e68350d916e',\n            'media_type': 'application/octet-stream',\n            'size': '3'\n        }\n        fixed_values['fee'] = {\n            'address': fixed_values.pop('fee_address'),\n            'amount': fixed_values.pop('fee_amount'),\n            'currency': fixed_values.pop('fee_currency').upper()\n        }\n\n        # create new stream with all fields set\n        tx = await self.out(self.stream_create('big', **values))\n        stream = tx['outputs'][0]['value']\n        fixed_values['source']['name'] = stream['source']['name']\n        fixed_values['source']['sd_hash'] = stream['source']['sd_hash']\n        self.assertEqual(stream, fixed_values)\n\n        # create stream with nothing set\n        tx = await self.out(self.stream_create('light'))\n        stream = tx['outputs'][0]['value']\n        self.assertEqual(\n            stream, {\n                'stream_type': 'binary',\n                'source': {\n                    'size': '3',\n                    'media_type': 'application/octet-stream',\n                    'name': stream['source']['name'],\n                    'hash': '56bf5dbae43f77a63d075b0f2ae9c7c3e3098db93779c7f9840da0f4db9c2f8c8454f4edd1373e2b64ee2e68350d916e',\n                    'sd_hash': stream['source']['sd_hash']\n                },\n            }\n        )\n\n        # create stream with just some tags, langs and locations\n        tx = await self.out(self.stream_create('updated', tags='blah', languages='uk', locations='UA::Kyiv'))\n        txo = tx['outputs'][0]\n        claim_id, stream = txo['claim_id'], txo['value']\n        fixed_values['source']['name'] = stream['source']['name']\n        fixed_values['source']['sd_hash'] = stream['source']['sd_hash']\n        self.assertEqual(\n            stream, {\n                'stream_type': 'binary',\n                'source': {\n                    'size': '3',\n                    'media_type': 'application/octet-stream',\n                    'name': fixed_values['source']['name'],\n                    'hash': '56bf5dbae43f77a63d075b0f2ae9c7c3e3098db93779c7f9840da0f4db9c2f8c8454f4edd1373e2b64ee2e68350d916e',\n                    'sd_hash': fixed_values['source']['sd_hash'],\n                },\n                'tags': ['blah'],\n                'languages': ['uk'],\n                'locations': [{'country': 'UA', 'city': 'Kyiv'}]\n            }\n        )\n\n        # update stream setting all fields, 'source' doesn't change\n        tx = await self.out(self.stream_update(claim_id, **values))\n        stream = tx['outputs'][0]['value']\n        fixed_values['tags'].insert(0, 'blah')  # existing tag\n        fixed_values['languages'].insert(0, 'uk')  # existing language\n        fixed_values['locations'].insert(0, {'country': 'UA', 'city': 'Kyiv'})  # existing location\n        self.assertEqual(stream, fixed_values)\n\n        # clearing and settings tags, languages and locations\n        tx = await self.out(self.stream_update(\n            claim_id, tags='single', clear_tags=True,\n            languages='pt', clear_languages=True,\n            locations='BR', clear_locations=True,\n        ))\n        txo = tx['outputs'][0]\n        fixed_values['tags'] = ['single']\n        fixed_values['languages'] = ['pt']\n        fixed_values['locations'] = [{'country': 'BR'}]\n        self.assertEqual(txo['value'], fixed_values)\n\n        # modifying hash/size/name\n        fixed_values['source']['name'] = 'changed_name'\n        fixed_values['source']['hash'] = 'cafebeef'\n        fixed_values['source']['size'] = '42'\n        tx = await self.out(self.stream_update(\n            claim_id, file_name='changed_name', file_hash='cafebeef', file_size=42\n        ))\n        self.assertEqual(tx['outputs'][0]['value'], fixed_values)\n\n        # stream_update re-signs with the same channel\n        channel_id = self.get_claim_id(await self.channel_create('@chan'))\n        tx = await self.stream_update(claim_id, channel_id=channel_id)\n        self.assertEqual(tx['outputs'][0]['signing_channel']['name'], '@chan')\n        tx = await self.stream_update(claim_id, title='channel re-signs')\n        self.assertEqual(tx['outputs'][0]['value']['title'], 'channel re-signs')\n        self.assertEqual(tx['outputs'][0]['signing_channel']['name'], '@chan')\n\n        # send claim to someone else\n        new_account = await self.out(self.daemon.jsonrpc_account_create('second account'))\n        account2_id, account2 = new_account['id'], self.wallet.get_account_or_error(new_account['id'])\n\n        # before sending\n        self.assertItemCount(await self.daemon.jsonrpc_claim_list(), 4)\n        self.assertItemCount(await self.daemon.jsonrpc_claim_list(account_id=self.account.id), 4)\n        self.assertItemCount(await self.daemon.jsonrpc_claim_list(account_id=account2_id), 0)\n\n        other_address = await account2.receiving.get_or_create_usable_address()\n        tx = await self.out(self.stream_update(claim_id, claim_address=other_address))\n\n        # after sending\n        self.assertItemCount(await self.daemon.jsonrpc_claim_list(), 4)\n        self.assertItemCount(await self.daemon.jsonrpc_claim_list(account_id=self.account.id), 3)\n        self.assertItemCount(await self.daemon.jsonrpc_claim_list(account_id=account2_id), 1)\n\n        self.assertEqual(4, len(await self.claim_search(release_time='>0', order_by=['release_time'])))\n        self.assertEqual(3, len(await self.claim_search(release_time='>0', order_by=['release_time'], claim_type='stream')))\n\n        self.assertEqual(4, len(await self.claim_search(release_time='>=0', order_by=['release_time'])))\n        self.assertEqual(4, len(await self.claim_search(order_by=['release_time'])))\n        self.assertEqual(3, len(await self.claim_search(claim_type='stream', order_by=['release_time'])))\n        self.assertEqual(1, len(await self.claim_search(claim_type='channel', order_by=['release_time'])))\n        self.assertEqual(2, len(await self.claim_search(release_time='>=123456', order_by=['release_time'])))\n\n        self.assertEqual(1, len(await self.claim_search(release_time='>=123456', order_by=['release_time'], claim_type='stream')))\n\n        self.assertEqual(1, len(await self.claim_search(release_time='>123456', order_by=['release_time'], claim_type='stream')))\n        self.assertEqual(2, len(await self.claim_search(release_time='>123456', order_by=['release_time'])))\n\n        self.assertEqual(3, len(await self.claim_search(release_time='<123457', order_by=['release_time'])))\n        self.assertEqual(2, len(await self.claim_search(release_time='<123457', order_by=['release_time'], claim_type='stream')))\n\n        self.assertEqual(2, len(await self.claim_search(release_time=['<123457'], order_by=['release_time'], claim_type='stream')))\n        self.assertEqual(3, len(await self.claim_search(release_time=['<123457'], order_by=['release_time'])))\n        self.assertEqual(3, len(await self.claim_search(release_time=['>0', '<123457'], order_by=['release_time'])))\n        self.assertEqual(2, len(await self.claim_search(release_time=['>0', '<123457'], order_by=['release_time'], claim_type='stream')))\n        self.assertEqual(3, len(await self.claim_search(release_time=['<123457'], order_by=['release_time'], height=['>0'])))\n        self.assertEqual(4, len(await self.claim_search(order_by=['release_time'], height=['>0'])))\n        self.assertEqual(4, len(await self.claim_search(order_by=['release_time'], height=['>0'], claim_type=['stream', 'channel'])))\n        self.assertEqual(\n            3, len(await self.claim_search(release_time=['>=123097', '<123457'], order_by=['release_time']))\n        )\n        self.assertEqual(\n            3, len(await self.claim_search(release_time=['<123457', '>0'], order_by=['release_time']))\n        )\n\n    async def test_setting_fee_fields(self):\n        tx = await self.out(self.stream_create('paid-stream'))\n        txo = tx['outputs'][0]\n        claim_id, stream = txo['claim_id'], txo['value']\n        fee_address = 'mmCsWAiXMUVecFQ3fVzUwvpT9XFMXno2Ca'\n\n        self.assertNotIn('fee', stream)\n\n        # --replace=false\n        # validation\n        with self.assertRaisesRegex(Exception, 'please specify a fee currency'):\n            await self.stream_update(claim_id, fee_amount='0.1')\n        with self.assertRaisesRegex(Exception, 'unknown currency provided: foo'):\n            await self.stream_update(claim_id, fee_amount='0.1', fee_currency=\"foo\")\n        with self.assertRaisesRegex(Exception, 'please specify a fee amount'):\n            await self.stream_update(claim_id, fee_currency='usd')\n        with self.assertRaisesRegex(Exception, 'please specify a fee amount'):\n            await self.stream_update(claim_id, fee_address=fee_address)\n        # set just amount and currency with default address\n        tx = await self.stream_update(\n            claim_id, fee_amount='0.99', fee_currency='lbc'\n        )\n        self.assertEqual(\n            tx['outputs'][0]['value']['fee'],\n            {'amount': '0.99', 'currency': 'LBC', 'address': txo['address']}\n        )\n        # set all fee fields\n        tx = await self.stream_update(\n            claim_id, fee_amount='0.1', fee_currency='usd', fee_address=fee_address\n        )\n        self.assertEqual(\n            tx['outputs'][0]['value']['fee'],\n            {'amount': '0.1', 'currency': 'USD', 'address': fee_address}\n        )\n        # change just address\n        tx = await self.stream_update(claim_id, fee_address=txo['address'])\n        self.assertEqual(\n            tx['outputs'][0]['value']['fee'],\n            {'amount': '0.1', 'currency': 'USD', 'address': txo['address']}\n        )\n        # change just amount (does not reset fee_address)\n        tx = await self.stream_update(claim_id, fee_amount='0.2')\n        self.assertEqual(\n            tx['outputs'][0]['value']['fee'],\n            {'amount': '0.2', 'currency': 'USD', 'address': txo['address']}\n        )\n        # changing currency without an amount is never allowed, even if previous amount exists\n        with self.assertRaises(Exception, msg='In order to set a fee currency, please specify a fee amount'):\n            await self.stream_update(claim_id, fee_currency='usd')\n        # clearing fee\n        tx = await self.out(self.stream_update(claim_id, clear_fee=True))\n        self.assertNotIn('fee', tx['outputs'][0]['value'])\n\n        # --replace=true\n        # set just amount and currency with default address\n        tx = await self.stream_update(\n            claim_id, fee_amount='0.99', fee_currency='lbc', replace=True\n        )\n        self.assertEqual(\n            tx['outputs'][0]['value']['fee'],\n            {'amount': '0.99', 'currency': 'LBC', 'address': txo['address']}\n        )\n        # set all fee fields\n        tx = await self.stream_update(\n            claim_id, fee_amount='0.1', fee_currency='usd', fee_address=fee_address, replace=True\n        )\n        self.assertEqual(\n            tx['outputs'][0]['value']['fee'],\n            {'amount': '0.1', 'currency': 'USD', 'address': fee_address}\n        )\n        # validation\n        with self.assertRaisesRegex(Exception, 'please specify a fee currency'):\n            await self.stream_update(claim_id, fee_amount='0.1', replace=True)\n        with self.assertRaisesRegex(Exception, 'unknown currency provided: foo'):\n            await self.stream_update(claim_id, fee_amount='0.1', fee_currency=\"foo\", replace=True)\n        with self.assertRaisesRegex(Exception, 'please specify a fee amount'):\n            await self.stream_update(claim_id, fee_currency='usd', replace=True)\n        with self.assertRaisesRegex(Exception, 'please specify a fee amount'):\n            await self.stream_update(claim_id, fee_address=fee_address, replace=True)\n\n    async def test_automatic_type_and_metadata_detection_for_image(self):\n        txo = (await self.stream_create('blank-image', data=self.image_data, suffix='.png'))['outputs'][0]\n        self.assertEqual(\n            txo['value'], {\n                'source': {\n                    'size': '99',\n                    'name': txo['value']['source']['name'],\n                    'media_type': 'image/png',\n                    'hash': '6c7df435d412c603390f593ef658c199817c7830ba3f16b7eadd8f99fa50e85dbd0d2b3dc61eadc33fe096e3872d1545',\n                    'sd_hash': txo['value']['source']['sd_hash'],\n                },\n                'stream_type': 'image',\n                'image': {\n                    'width': 5,\n                    'height': 7\n                }\n            }\n        )\n\n    async def test_automatic_type_and_metadata_detection_for_video(self):\n        txo = (await self.stream_create('chrome', file_path=self.video_file_name))['outputs'][0]\n        self.assertEqual(\n            txo['value'], {\n                'source': {\n                    'size': '2299653',\n                    'name': 'ForBiggerEscapes.mp4',\n                    'media_type': 'video/mp4',\n                    'hash': '5f6811c83c1616df06f10bf5309ca61edb5ff949a9c1212ce784602d837bfdfc1c3db1e0580ef7bd1dadde41d8acf315',\n                    'sd_hash': txo['value']['source']['sd_hash'],\n                },\n                'stream_type': 'video',\n                'video': {\n                    'width': 1280,\n                    'height': 720,\n                    'duration': 15\n                }\n            }\n        )\n\n    async def test_overriding_automatic_metadata_detection(self):\n        tx = await self.out(\n            self.daemon.jsonrpc_stream_create(\n                'chrome', '1.0', file_path=self.video_file_name, width=99, height=88, duration=9\n            )\n        )\n        txo = tx['outputs'][0]\n        self.assertEqual(\n            txo['value'], {\n                'source': {\n                    'size': '2299653',\n                    'name': 'ForBiggerEscapes.mp4',\n                    'media_type': 'video/mp4',\n                    'hash': '5f6811c83c1616df06f10bf5309ca61edb5ff949a9c1212ce784602d837bfdfc1c3db1e0580ef7bd1dadde41d8acf315',\n                    'sd_hash': txo['value']['source']['sd_hash'],\n                },\n                'stream_type': 'video',\n                'video': {\n                    'width': 99,\n                    'height': 88,\n                    'duration': 9\n                }\n            }\n        )\n\n    async def test_update_file_type(self):\n        video_txo = (await self.stream_create('chrome', file_path=self.video_file_name))['outputs'][0]\n        self.assertSetEqual(set(video_txo['value']), {'source', 'stream_type', 'video'})\n        self.assertEqual(video_txo['value']['stream_type'], 'video')\n        self.assertEqual(video_txo['value']['source']['media_type'], 'video/mp4')\n        self.assertEqual(\n            video_txo['value']['video'], {\n                'duration': 15,\n                'height': 720,\n                'width': 1280\n            }\n        )\n        claim_id = video_txo['claim_id']\n\n        binary_txo = (await self.stream_update(claim_id, data=b'hi!'))['outputs'][0]\n        self.assertEqual(binary_txo['value']['stream_type'], 'binary')\n        self.assertEqual(binary_txo['value']['source']['media_type'], 'application/octet-stream')\n        self.assertSetEqual(set(binary_txo['value']), {'source', 'stream_type'})\n\n        image_txo = (await self.stream_update(claim_id, data=self.image_data, suffix='.png'))['outputs'][0]\n        self.assertSetEqual(set(image_txo['value']), {'source', 'stream_type', 'image'})\n        self.assertEqual(image_txo['value']['stream_type'], 'image')\n        self.assertEqual(image_txo['value']['source']['media_type'], 'image/png')\n        self.assertEqual(image_txo['value']['image'], {'height': 7, 'width': 5})\n\n    async def test_replace_mode_preserves_source_and_type(self):\n        expected = {\n            'tags': ['blah'],\n            'languages': ['uk'],\n            'locations': [{'country': 'UA', 'city': 'Kyiv'}],\n            'source': {\n                'size': '2299653',\n                'name': 'ForBiggerEscapes.mp4',\n                'media_type': 'video/mp4',\n                'hash': '5f6811c83c1616df06f10bf5309ca61edb5ff949a9c1212ce784602d837bfdfc1c3db1e0580ef7bd1dadde41d8acf315',\n            },\n            'stream_type': 'video',\n            'video': {\n                'width': 1280,\n                'height': 720,\n                'duration': 15\n            }\n        }\n        channel = await self.channel_create('@chan')\n        tx = await self.out(self.daemon.jsonrpc_stream_create(\n            'chrome', '1.0', file_path=self.video_file_name,\n            tags='blah', languages='uk', locations='UA::Kyiv',\n            channel_id=self.get_claim_id(channel)\n        ))\n        await self.on_transaction_dict(tx)\n        txo = tx['outputs'][0]\n        expected['source']['sd_hash'] = txo['value']['source']['sd_hash']\n        self.assertEqual(txo['value'], expected)\n        self.assertEqual(txo['signing_channel']['name'], '@chan')\n        tx = await self.out(self.daemon.jsonrpc_stream_update(\n            txo['claim_id'], title='new title', replace=True\n        ))\n        txo = tx['outputs'][0]\n        expected['title'] = 'new title'\n        del expected['tags']\n        del expected['languages']\n        del expected['locations']\n        self.assertEqual(txo['value'], expected)\n        self.assertNotIn('signing_channel', txo)\n\n    async def test_create_update_and_abandon_stream(self):\n        await self.assertBalance(self.account, '10.0')\n\n        tx = await self.stream_create(bid='2.5')  # creates new claim\n        claim_id = self.get_claim_id(tx)\n        txs = await self.transaction_list()\n        self.assertEqual(len(txs[0]['claim_info']), 1)\n        self.assertEqual(txs[0]['confirmations'], 1)\n        self.assertEqual(txs[0]['claim_info'][0]['balance_delta'], '-2.5')\n        self.assertEqual(txs[0]['claim_info'][0]['claim_id'], claim_id)\n        self.assertFalse(txs[0]['claim_info'][0]['is_spent'])\n        self.assertEqual(txs[0]['value'], '0.0')\n        self.assertEqual(txs[0]['fee'], '-0.020107')\n        await self.assertBalance(self.account, '7.479893')\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 1)\n\n        await self.daemon.jsonrpc_file_delete(delete_all=True)\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 0)\n\n        await self.stream_update(claim_id, bid='1.0')  # updates previous claim\n        txs = await self.transaction_list()\n        self.assertEqual(len(txs[0]['update_info']), 1)\n        self.assertEqual(txs[0]['update_info'][0]['balance_delta'], '1.5')\n        self.assertEqual(txs[0]['update_info'][0]['claim_id'], claim_id)\n        self.assertFalse(txs[0]['update_info'][0]['is_spent'])\n        self.assertTrue(txs[1]['claim_info'][0]['is_spent'])\n        self.assertEqual(txs[0]['value'], '0.0')\n        self.assertEqual(txs[0]['fee'], '-0.0002165')\n        await self.assertBalance(self.account, '8.9796765')\n\n        await self.stream_abandon(claim_id)\n        txs = await self.transaction_list()\n        self.assertEqual(len(txs[0]['abandon_info']), 1)\n        self.assertEqual(txs[0]['abandon_info'][0]['balance_delta'], '1.0')\n        self.assertEqual(txs[0]['abandon_info'][0]['claim_id'], claim_id)\n        self.assertTrue(txs[1]['update_info'][0]['is_spent'])\n        self.assertTrue(txs[2]['claim_info'][0]['is_spent'])\n        self.assertEqual(txs[0]['value'], '0.0')\n        self.assertEqual(txs[0]['fee'], '-0.000107')\n        await self.assertBalance(self.account, '9.9795695')\n\n    async def test_abandoning_stream_at_loss(self):\n        await self.assertBalance(self.account, '10.0')\n        tx = await self.stream_create(bid='0.0001')\n        await self.assertBalance(self.account, '9.979793')\n        await self.stream_abandon(self.get_claim_id(tx))\n        await self.assertBalance(self.account, '9.97968399')\n\n    async def test_publish(self):\n\n        # errors on missing arguments to create a stream\n        with self.assertRaisesRegex(Exception, \"'bid' is a required argument for new publishes.\"):\n            await self.daemon.jsonrpc_publish('foo')\n\n        # successfully create stream\n        with tempfile.NamedTemporaryFile() as file:\n            file.write(b'hi')\n            file.flush()\n            tx1 = await self.publish('foo', bid='1.0', file_path=file.name)\n\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 1)\n\n        # doesn't error on missing arguments when doing an update stream\n        tx2 = await self.publish('foo', tags='updated')\n\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 1)\n        self.assertEqual(self.get_claim_id(tx1), self.get_claim_id(tx2))\n\n        # update conflict with two claims of the same name\n        tx3 = await self.stream_create('foo', allow_duplicate_name=True)\n        with self.assertRaisesRegex(Exception, \"There are 2 claims for 'foo'\"):\n            await self.daemon.jsonrpc_publish('foo')\n\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 2)\n        # abandon duplicate stream\n        await self.stream_abandon(self.get_claim_id(tx3))\n\n        # publish to a channel\n        await self.channel_create('@abc')\n        tx3 = await self.publish('foo', channel_name='@abc')\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 2)\n        r = await self.resolve('lbry://@abc/foo')\n        self.assertEqual(\n            r['claim_id'],\n            self.get_claim_id(tx3)\n        )\n\n        # publishing again clears channel\n        tx4 = await self.publish('foo', languages='uk-UA', tags=['Anime', 'anime '])\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 2)\n        claim = await self.resolve('lbry://foo')\n        self.assertEqual(claim['txid'], tx4['outputs'][0]['txid'])\n        self.assertNotIn('signing_channel', claim)\n        self.assertEqual(claim['value']['languages'], ['uk-UA'])\n        self.assertEqual(claim['value']['tags'], ['anime'])\n\n        # publish a stream with no source\n        tx5 = await self.publish(\n            'future-release', bid='0.1', languages='uk-UA', tags=['Anime', 'anime ']\n        )\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 2)\n        claim = await self.resolve('lbry://future-release')\n        self.assertEqual(claim['txid'], tx5['outputs'][0]['txid'])\n        self.assertNotIn('signing_channel', claim)\n        self.assertEqual(claim['value']['languages'], ['uk-UA'])\n        self.assertEqual(claim['value']['tags'], ['anime'])\n        self.assertNotIn('source', claim['value'])\n\n        # change metadata before the release\n        await self.publish(\n            'future-release', bid='0.1', tags=['Anime', 'anime ', 'psy-trance'], title='Psy will be over 9000!!!'\n        )\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 2)\n        claim = await self.resolve('lbry://future-release')\n        self.assertEqual(claim['value']['tags'], ['anime', 'psy-trance'])\n        self.assertEqual(claim['value']['title'], 'Psy will be over 9000!!!')\n        self.assertNotIn('source', claim['value'])\n\n        # update the stream to have a source\n        tx6 = await self.publish(\n            'future-release', sd_hash='beef', file_hash='beef',\n            file_name='blah.mp3', tags=['something-else']\n        )\n        claim = await self.resolve('lbry://future-release')\n        self.assertEqual(claim['txid'], tx6['outputs'][0]['txid'])\n        self.assertEqual(claim['value']['tags'], ['something-else'])\n        self.assertEqual(claim['value']['source']['sd_hash'], 'beef')\n        self.assertEqual(claim['value']['source']['hash'], 'beef')\n        self.assertEqual(claim['value']['source']['name'], 'blah.mp3')\n        self.assertEqual(claim['value']['source']['media_type'], 'audio/mpeg')\n\n\nclass SupportCommands(CommandTestCase):\n\n    async def test_regular_supports_and_tip_supports(self):\n        wallet2 = await self.daemon.jsonrpc_wallet_create('wallet2', create_account=True)\n        account2 = wallet2.accounts[0]\n\n        # send account2 5 LBC out of the 10 LBC in account1\n        result = await self.out(self.daemon.jsonrpc_account_send(\n            '5.0', await self.daemon.jsonrpc_address_unused(wallet_id='wallet2')\n        ))\n        await self.on_transaction_dict(result)\n\n        # account1 and account2 balances:\n        await self.assertBalance(self.account, '4.999876')\n        await self.assertBalance(account2,     '5.0')\n\n        # create the claim we'll be tipping and supporting\n        claim_id = self.get_claim_id(await self.stream_create())\n\n        # account1 and account2 balances:\n        await self.assertBalance(self.account, '3.979769')\n        await self.assertBalance(account2,     '5.0')\n\n        # send a tip to the claim using account2\n        tip = await self.out(\n            self.daemon.jsonrpc_support_create(\n                claim_id, '1.0', True, account_id=account2.id, wallet_id='wallet2',\n                funding_account_ids=[account2.id], blocking=True)\n        )\n        await self.confirm_tx(tip['txid'])\n\n        # tips don't affect balance so account1 balance is same but account2 balance went down\n        await self.assertBalance(self.account, '3.979769')\n        await self.assertBalance(account2,     '3.9998585')\n\n        # verify that the incoming tip is marked correctly as is_tip=True in account1\n        txs = await self.transaction_list(account_id=self.account.id)\n        self.assertEqual(len(txs[0]['support_info']), 1)\n        self.assertEqual(txs[0]['support_info'][0]['balance_delta'], '1.0')\n        self.assertEqual(txs[0]['support_info'][0]['claim_id'], claim_id)\n        self.assertTrue(txs[0]['support_info'][0]['is_tip'])\n        self.assertFalse(txs[0]['support_info'][0]['is_spent'])\n        self.assertEqual(txs[0]['value'], '1.0')\n        self.assertEqual(txs[0]['fee'], '0.0')\n\n        # verify that the outgoing tip is marked correctly as is_tip=True in account2\n        txs2 = await self.transaction_list(wallet_id='wallet2', account_id=account2.id)\n        self.assertEqual(len(txs2[0]['support_info']), 1)\n        self.assertEqual(txs2[0]['support_info'][0]['balance_delta'], '-1.0')\n        self.assertEqual(txs2[0]['support_info'][0]['claim_id'], claim_id)\n        self.assertTrue(txs2[0]['support_info'][0]['is_tip'])\n        self.assertFalse(txs2[0]['support_info'][0]['is_spent'])\n        self.assertEqual(txs2[0]['value'], '-1.0')\n        self.assertEqual(txs2[0]['fee'], '-0.0001415')\n\n        # send a support to the claim using account2\n        support = await self.out(\n            self.daemon.jsonrpc_support_create(\n                claim_id, '2.0', False, account_id=account2.id, wallet_id='wallet2',\n                funding_account_ids=[account2.id], blocking=True)\n        )\n        await self.confirm_tx(support['txid'])\n\n        # account2 balance went down ~2\n        await self.assertBalance(self.account, '3.979769')\n        await self.assertBalance(account2,     '1.999717')\n\n        # verify that the outgoing support is marked correctly as is_tip=False in account2\n        txs2 = await self.transaction_list(wallet_id='wallet2')\n        self.assertEqual(len(txs2[0]['support_info']), 1)\n        self.assertEqual(txs2[0]['support_info'][0]['balance_delta'], '-2.0')\n        self.assertEqual(txs2[0]['support_info'][0]['claim_id'], claim_id)\n        self.assertFalse(txs2[0]['support_info'][0]['is_tip'])\n        self.assertFalse(txs2[0]['support_info'][0]['is_spent'])\n        self.assertEqual(txs2[0]['value'], '0.0')\n        self.assertEqual(txs2[0]['fee'], '-0.0001415')\n\n        # abandoning the tip increases balance and shows tip as spent\n        await self.support_abandon(claim_id)\n        await self.assertBalance(self.account, '4.979662')\n        txs = await self.transaction_list(account_id=self.account.id)\n        self.assertEqual(len(txs[0]['abandon_info']), 1)\n        self.assertEqual(len(txs[1]['support_info']), 1)\n        self.assertTrue(txs[1]['support_info'][0]['is_tip'])\n        self.assertTrue(txs[1]['support_info'][0]['is_spent'])\n\n    async def test_signed_supports_with_no_change_txo_regression(self):\n        # reproduces a bug where transactions did not get properly signed\n        # if there was no change and just a single output\n        # lbrycrd returned 'the transaction was rejected by network rules.'\n        channel_id = self.get_claim_id(await self.channel_create())\n        stream_id = self.get_claim_id(await self.stream_create())\n        tx = await self.support_create(stream_id, '7.967601', channel_id=channel_id)\n        self.assertEqual(len(tx['outputs']), 1)  # must be one to reproduce bug\n        self.assertTrue(tx['outputs'][0]['is_channel_signature_valid'])\n\n\nclass CollectionCommands(CommandTestCase):\n\n    async def test_collections(self):\n        claim_ids = [\n            self.get_claim_id(tx) for tx in [\n                await self.stream_create('stream-one'),\n                await self.stream_create('stream-two')\n            ]\n        ]\n        claim_ids.append(claim_ids[0])\n        claim_ids.append('beef')\n        tx = await self.collection_create('radjingles', claims=claim_ids, title=\"boring title\")\n        claim_id = self.get_claim_id(tx)\n        collections = await self.out(self.daemon.jsonrpc_collection_list())\n        self.assertEqual(collections['items'][0]['value']['title'], 'boring title')\n        self.assertEqual(collections['items'][0]['value']['claims'], claim_ids)\n        self.assertEqual(collections['items'][0]['value_type'], 'collection')\n\n        self.assertItemCount(collections, 1)\n        await self.assertBalance(self.account, '6.939679')\n\n        with self.assertRaisesRegex(Exception, \"You already have a collection under the name 'radjingles'.\"):\n            await self.collection_create('radjingles', claims=claim_ids)\n\n        self.assertItemCount(await self.daemon.jsonrpc_collection_list(), 1)\n        await self.assertBalance(self.account, '6.939679')\n\n        collections = await self.out(self.daemon.jsonrpc_collection_list())\n        self.assertEqual(collections['items'][0]['value']['title'], 'boring title')\n        await self.collection_update(claim_id, title='fancy title')\n        collections = await self.out(self.daemon.jsonrpc_collection_list())\n        self.assertEqual(collections['items'][0]['value']['title'], 'fancy title')\n        self.assertEqual(collections['items'][0]['value']['claims'], claim_ids)\n        self.assertNotIn('claims', collections['items'][0])\n\n        tx = await self.collection_create('radjingles', claims=claim_ids, allow_duplicate_name=True)\n        claim_id2 = self.get_claim_id(tx)\n        self.assertItemCount(await self.daemon.jsonrpc_collection_list(), 2)\n        # with clear_claims\n        await self.collection_update(claim_id, clear_claims=True, claims=claim_ids[:2])\n        collections = await self.out(self.daemon.jsonrpc_collection_list())\n        self.assertEqual(len(collections['items']), 2)\n        self.assertNotIn('canonical_url', collections['items'][0])\n\n        resolved_collections = await self.out(self.daemon.jsonrpc_collection_list(resolve=True))\n        self.assertIn('canonical_url', resolved_collections['items'][0])\n        # with replace\n        await self.collection_update(claim_id, replace=True, claims=claim_ids[::-1][:2], tags=['cool'])\n        updated = await self.claim_search(claim_id=claim_id)\n        self.assertEqual(updated[0]['value']['tags'], ['cool'])\n        self.assertEqual(updated[0]['value']['claims'], claim_ids[::-1][:2])\n        await self.collection_update(claim_id, replace=True, claims=claim_ids[:4], languages=['en', 'pt-BR'])\n        updated = await self.resolve(f'radjingles:{claim_id}')\n        self.assertEqual(updated['value']['claims'], claim_ids[:4])\n        self.assertNotIn('tags', updated['value'])\n        self.assertEqual(updated['value']['languages'], ['en', 'pt-BR'])\n\n        await self.collection_abandon(claim_id)\n        self.assertItemCount(await self.daemon.jsonrpc_collection_list(), 1)\n\n        collections = await self.out(self.daemon.jsonrpc_collection_list(resolve_claims=2))\n        self.assertEqual(len(collections['items'][0]['claims']), 2)\n\n        collections = await self.out(self.daemon.jsonrpc_collection_list(resolve_claims=10))\n        self.assertEqual(len(collections['items'][0]['claims']), 4)\n        self.assertEqual(collections['items'][0]['claims'][0]['name'], 'stream-one')\n        self.assertEqual(collections['items'][0]['claims'][1]['name'], 'stream-two')\n        self.assertEqual(collections['items'][0]['claims'][2]['name'], 'stream-one')\n        self.assertIsNone(collections['items'][0]['claims'][3])\n\n        claims = await self.out(self.daemon.jsonrpc_claim_list())\n        self.assertEqual(claims['items'][0]['name'], 'radjingles')\n        self.assertEqual(claims['items'][1]['name'], 'stream-two')\n        self.assertEqual(claims['items'][2]['name'], 'stream-one')\n\n        claims = await self.out(self.daemon.jsonrpc_collection_resolve(claim_id2))\n        self.assertEqual(claims['items'][0]['name'], 'stream-one')\n        self.assertEqual(claims['items'][1]['name'], 'stream-two')\n        self.assertEqual(claims['items'][2]['name'], 'stream-one')\n\n        claims = await self.out(self.daemon.jsonrpc_collection_resolve(claim_id2, page=10))\n        self.assertEqual(claims['items'], [])\n"
  },
  {
    "path": "tests/integration/datanetwork/__init__.py",
    "content": ""
  },
  {
    "path": "tests/integration/datanetwork/test_dht.py",
    "content": "import asyncio\nfrom binascii import hexlify\n\nfrom lbry.extras.daemon.storage import SQLiteStorage\nfrom lbry.conf import Config\nfrom lbry.dht import constants\nfrom lbry.dht.node import Node\nfrom lbry.dht import peer as dht_peer\nfrom lbry.dht.peer import PeerManager, make_kademlia_peer\nfrom lbry.testcase import AsyncioTestCase\n\n\nclass DHTIntegrationTest(AsyncioTestCase):\n\n    async def asyncSetUp(self):\n        dht_peer.ALLOW_LOCALHOST = True\n        self.addCleanup(setattr, dht_peer, 'ALLOW_LOCALHOST', False)\n        import logging\n        logging.getLogger('asyncio').setLevel(logging.ERROR)\n        logging.getLogger('lbry.dht').setLevel(logging.WARN)\n        self.nodes = []\n        self.known_node_addresses = []\n\n    async def create_node(self, node_id, port, external_ip='127.0.0.1'):\n        storage = SQLiteStorage(Config(), \":memory:\", self.loop, self.loop.time)\n        await storage.open()\n        node = Node(self.loop, PeerManager(self.loop), node_id=node_id,\n                    udp_port=port, internal_udp_port=port,\n                    peer_port=3333, external_ip=external_ip,\n                    storage=storage)\n        self.addCleanup(node.stop)\n        node.protocol.rpc_timeout = .5\n        node.protocol.ping_queue._default_delay = .5\n        return node\n\n    async def setup_network(self, size: int, start_port=40000, seed_nodes=1, external_ip='127.0.0.1'):\n        for i in range(size):\n            node_port = start_port + i\n            node_id = constants.generate_id(i)\n            node = await self.create_node(node_id, node_port)\n            self.nodes.append(node)\n            self.known_node_addresses.append((external_ip, node_port))\n\n        for node in self.nodes:\n            node.start(external_ip, self.known_node_addresses[:seed_nodes])\n\n    async def test_replace_bad_nodes(self):\n        await self.setup_network(20)\n        await asyncio.gather(*[node.joined.wait() for node in self.nodes])\n        self.assertEqual(len(self.nodes), 20)\n        node = self.nodes[0]\n        bad_peers = []\n        for candidate in self.nodes[1:10]:\n            address, port, node_id = candidate.protocol.external_ip, candidate.protocol.udp_port, candidate.protocol.node_id\n            peer = make_kademlia_peer(node_id, address, udp_port=port)\n            bad_peers.append(peer)\n            node.protocol.add_peer(peer)\n            candidate.stop()\n        await asyncio.sleep(.3)  # let pending events settle\n        for bad_peer in bad_peers:\n            self.assertIn(bad_peer, node.protocol.routing_table.get_peers())\n        await node.refresh_node(True)\n        await asyncio.sleep(.3)  # let pending events settle\n        good_nodes = {good_node.protocol.node_id for good_node in self.nodes[10:]}\n        for peer in node.protocol.routing_table.get_peers():\n            self.assertIn(peer.node_id, good_nodes)\n\n    async def test_re_join(self):\n        await self.setup_network(20, seed_nodes=10)\n        await asyncio.gather(*[node.joined.wait() for node in self.nodes])\n        node = self.nodes[-1]\n        self.assertTrue(node.joined.is_set())\n        self.assertTrue(node.protocol.routing_table.get_peers())\n        for network_node in self.nodes[:-1]:\n            network_node.stop()\n        await node.refresh_node(True)\n        await asyncio.sleep(.3)  # let pending events settle\n        self.assertFalse(node.protocol.routing_table.get_peers())\n        for network_node in self.nodes[:-1]:\n            network_node.start('127.0.0.1', self.known_node_addresses)\n        self.assertFalse(node.protocol.routing_table.get_peers())\n        timeout = 20\n        while not node.protocol.routing_table.get_peers():\n            await asyncio.sleep(.1)\n            timeout -= 1\n            if not timeout:\n                self.fail(\"node didn't join back after 2 seconds\")\n\n    async def test_announce_no_peers(self):\n        await self.setup_network(1)\n        node = self.nodes[0]\n        blob_hash = hexlify(constants.generate_id(1337)).decode()\n        peers = await node.announce_blob(blob_hash)\n        self.assertEqual(len(peers), 0)\n\n    async def test_get_token_on_announce(self):\n        await self.setup_network(2, seed_nodes=2)\n        await asyncio.gather(*[node.joined.wait() for node in self.nodes])\n        node1, node2 = self.nodes\n        node1.protocol.peer_manager.clear_token(node2.protocol.node_id)\n        blob_hash = hexlify(constants.generate_id(1337)).decode()\n        node_ids = await node1.announce_blob(blob_hash)\n        self.assertIn(node2.protocol.node_id, node_ids)\n        node2.protocol.node_rpc.refresh_token()\n        node_ids = await node1.announce_blob(blob_hash)\n        self.assertIn(node2.protocol.node_id, node_ids)\n        node2.protocol.node_rpc.refresh_token()\n        node_ids = await node1.announce_blob(blob_hash)\n        self.assertIn(node2.protocol.node_id, node_ids)\n\n    async def test_peer_search_removes_bad_peers(self):\n        # that's an edge case discovered by Tom, but an important one\n        # imagine that you only got bad peers and refresh will happen in one hour\n        # instead of failing for one hour we should be able to recover by scheduling pings to bad peers we find\n        await self.setup_network(2, seed_nodes=2)\n        await asyncio.gather(*[node.joined.wait() for node in self.nodes])\n        node1, node2 = self.nodes\n        node2.stop()\n        # forcefully make it a bad peer but don't remove it from routing table\n        address, port, node_id = node2.protocol.external_ip, node2.protocol.udp_port, node2.protocol.node_id\n        peer = make_kademlia_peer(node_id, address, udp_port=port)\n        self.assertTrue(node1.protocol.peer_manager.peer_is_good(peer))\n        node1.protocol.peer_manager.report_failure(node2.protocol.external_ip, node2.protocol.udp_port)\n        node1.protocol.peer_manager.report_failure(node2.protocol.external_ip, node2.protocol.udp_port)\n        self.assertFalse(node1.protocol.peer_manager.peer_is_good(peer))\n\n        # now a search happens, which removes bad peers while contacting them\n        self.assertTrue(node1.protocol.routing_table.get_peers())\n        await node1.peer_search(node2.protocol.node_id)\n        await asyncio.sleep(.3)  # let pending events settle\n        self.assertFalse(node1.protocol.routing_table.get_peers())\n\n    async def test_peer_persistance(self):\n        num_nodes = 6\n        start_port = 40000\n        num_seeds = 2\n        external_ip = '127.0.0.1'\n\n        # Start a node\n        await self.setup_network(num_nodes, start_port=start_port, seed_nodes=num_seeds)\n        await asyncio.gather(*[node.joined.wait() for node in self.nodes])\n\n        node1 = self.nodes[-1]\n        peer_args = [(n.protocol.node_id, n.protocol.external_ip, n.protocol.udp_port, n.protocol.peer_port) for n in\n                     self.nodes[:num_seeds]]\n        peers = [make_kademlia_peer(*args) for args in peer_args]\n\n        # node1 is bootstrapped from the fixed seeds\n        self.assertCountEqual(peers, node1.protocol.routing_table.get_peers())\n\n        # Refresh and assert that the peers were persisted\n        await node1.refresh_node(True)\n        self.assertEqual(len(peer_args), len(await node1._storage.get_persisted_kademlia_peers()))\n        node1.stop()\n\n        # Start a fresh node with the same node_id and storage, but no known peers\n        node2 = await self.create_node(constants.generate_id(num_nodes-1), start_port+num_nodes-1)\n        node2._storage = node1._storage\n        node2.start(external_ip, [])\n        await node2.joined.wait()\n\n        # The peers are restored\n        self.assertEqual(num_seeds, len(node2.protocol.routing_table.get_peers()))\n        for bucket1, bucket2 in zip(node1.protocol.routing_table.buckets, node2.protocol.routing_table.buckets):\n            self.assertEqual((bucket1.range_min, bucket1.range_max), (bucket2.range_min, bucket2.range_max))\n"
  },
  {
    "path": "tests/integration/datanetwork/test_file_commands.py",
    "content": "import unittest\nfrom unittest import skipIf\nimport asyncio\nimport os\nfrom binascii import hexlify\n\nfrom lbry.schema import Claim\nfrom lbry.stream.background_downloader import BackgroundDownloader\nfrom lbry.stream.descriptor import StreamDescriptor\nfrom lbry.testcase import CommandTestCase\nfrom lbry.extras.daemon.components import TorrentSession, BACKGROUND_DOWNLOADER_COMPONENT\nfrom lbry.wallet import Transaction\nfrom lbry.torrent.tracker import UDPTrackerServerProtocol\n\n\nclass FileCommands(CommandTestCase):\n    def __init__(self, *a, **kw):\n        super().__init__(*a, **kw)\n        self.skip_libtorrent = False\n\n    async def add_forever(self):\n        while True:\n            for handle in self.client_session._handles.values():\n                handle._handle.connect_peer(('127.0.0.1', 4040))\n            await asyncio.sleep(.1)\n\n    async def initialize_torrent(self, tx_to_update=None):\n        if not hasattr(self, 'seeder_session'):\n            self.seeder_session = TorrentSession(self.loop, None)\n            self.addCleanup(self.seeder_session.stop)\n            await self.seeder_session.bind('127.0.0.1', port=4040)\n        btih = await self.seeder_session.add_fake_torrent()\n        address = await self.account.receiving.get_or_create_usable_address()\n        if not tx_to_update:\n            claim = Claim()\n            claim.stream.update(bt_infohash=btih)\n            tx = await Transaction.claim_create(\n                'torrent', claim, 1, address, [self.account], self.account\n            )\n        else:\n            claim = tx_to_update.outputs[0].claim\n            claim.stream.update(bt_infohash=btih)\n            tx = await Transaction.claim_update(\n                tx_to_update.outputs[0], claim, 1, address, [self.account], self.account\n            )\n        await tx.sign([self.account])\n        await self.broadcast_and_confirm(tx)\n        self.client_session = self.daemon.file_manager.source_managers['torrent'].torrent_session\n        self.client_session.wait_start = False  # fixme: this is super slow on tests\n        task = asyncio.create_task(self.add_forever())\n        self.addCleanup(task.cancel)\n        return tx, btih\n\n    @skipIf(TorrentSession is None, \"libtorrent not installed\")\n    async def test_download_torrent(self):\n        tx, btih = await self.initialize_torrent()\n        self.assertNotIn('error', await self.out(self.daemon.jsonrpc_get('torrent')))\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 1)\n        # second call, see its there and move on\n        self.assertNotIn('error', await self.out(self.daemon.jsonrpc_get('torrent')))\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 1)\n        self.assertEqual((await self.daemon.jsonrpc_file_list())['items'][0].identifier, btih)\n        self.assertIn(btih, self.client_session._handles)\n        tx, new_btih = await self.initialize_torrent(tx)\n        self.assertNotEqual(btih, new_btih)\n        # claim now points to another torrent, update to it\n        self.assertNotIn('error', await self.out(self.daemon.jsonrpc_get('torrent')))\n        self.assertEqual((await self.daemon.jsonrpc_file_list())['items'][0].identifier, new_btih)\n        self.assertIn(new_btih, self.client_session._handles)\n        self.assertNotIn(btih, self.client_session._handles)\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 1)\n        await self.daemon.jsonrpc_file_delete(delete_all=True)\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 0)\n        self.assertNotIn(new_btih, self.client_session._handles)\n\n    async def create_streams_in_range(self, *args, **kwargs):\n        self.stream_claim_ids = []\n        for i in range(*args, **kwargs):\n            t = await self.stream_create(f'Stream_{i}', '0.00001')\n            self.stream_claim_ids.append(t['outputs'][0]['claim_id'])\n\n    async def test_file_reflect(self):\n        tx = await self.stream_create('mirror', '0.01')\n        sd_hash = tx['outputs'][0]['value']['source']['sd_hash']\n        self.assertEqual([], await self.daemon.jsonrpc_file_reflect(sd_hash=sd_hash))\n        all_except_sd = [\n            blob_hash for blob_hash in self.server.blob_manager.completed_blob_hashes if blob_hash != sd_hash\n        ]\n        await self.reflector.blob_manager.delete_blobs(all_except_sd)\n        self.assertEqual(all_except_sd, await self.daemon.jsonrpc_file_reflect(sd_hash=sd_hash))\n\n    async def test_sd_blob_fields_fallback(self):\n        claim_id = self.get_claim_id(await self.stream_create('foo', '0.01', suffix='.txt'))\n        stream = (await self.daemon.jsonrpc_file_list())[\"items\"][0]\n        stream.descriptor.suggested_file_name = ' '\n        stream.descriptor.stream_name = ' '\n        stream.descriptor.stream_hash = stream.descriptor.get_stream_hash()\n        sd_hash = stream.descriptor.sd_hash = stream.descriptor.calculate_sd_hash()\n        await stream.descriptor.make_sd_blob()\n        await self.daemon.jsonrpc_file_delete(claim_name='foo')\n        await self.stream_update(claim_id=claim_id, sd_hash=sd_hash)\n        file_dict = await self.out(self.daemon.jsonrpc_get('lbry://foo', save_file=True))\n        self.assertEqual(file_dict['suggested_file_name'], stream.file_name)\n        self.assertEqual(file_dict['stream_name'], stream.file_name)\n        self.assertEqual(file_dict['mime_type'], 'text/plain')\n\n    async def test_file_management(self):\n        await self.stream_create('foo', '0.01')\n        await self.stream_create('foo2', '0.01')\n\n        file1, file2 = await self.file_list('claim_name')\n        self.assertEqual(file1['claim_name'], 'foo')\n        self.assertEqual(file2['claim_name'], 'foo2')\n\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(claim_id=[file1['claim_id'], file2['claim_id']]), 2)\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(claim_id=file1['claim_id']), 1)\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(outpoint=[file1['outpoint'], file2['outpoint']]), 2)\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(outpoint=file1['outpoint']), 1)\n\n        await self.daemon.jsonrpc_file_delete(claim_name='foo')\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 1)\n        await self.daemon.jsonrpc_file_delete(claim_name='foo2')\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 0)\n\n        await self.daemon.jsonrpc_get('lbry://foo')\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 1)\n\n    async def test_tracker_discovery(self):\n        port = 50990\n        server = UDPTrackerServerProtocol()\n        transport, _ = await self.loop.create_datagram_endpoint(lambda: server, local_addr=(\"127.0.0.1\", port))\n        self.addCleanup(transport.close)\n        self.daemon.conf.fixed_peers = []\n        self.daemon.conf.tracker_servers = [(\"127.0.0.1\", port)]\n        tx = await self.stream_create('foo', '0.01')\n        sd_hash = tx['outputs'][0]['value']['source']['sd_hash']\n        self.assertNotIn(bytes.fromhex(sd_hash)[:20], server.peers)\n        server.add_peer(bytes.fromhex(sd_hash)[:20], \"127.0.0.1\", 5567)\n        self.assertEqual(1, len(server.peers[bytes.fromhex(sd_hash)[:20]]))\n        self.assertTrue(await self.daemon.jsonrpc_file_delete(delete_all=True))\n        stream = await self.daemon.jsonrpc_get('foo', save_file=True)\n        await self.wait_files_to_complete()\n        self.assertEqual(0, stream.blobs_remaining)\n        self.assertEqual(2, len(server.peers[bytes.fromhex(sd_hash)[:20]]))\n        self.assertEqual([{'address': '127.0.0.1',\n                           'node_id': None,\n                           'tcp_port': 5567,\n                           'udp_port': None},\n                          {'address': '127.0.0.1',\n                           'node_id': None,\n                           'tcp_port': 4444,\n                           'udp_port': None}], (await self.daemon.jsonrpc_peer_list(sd_hash))['items'])\n\n    async def test_announces(self):\n        # announces on publish\n        self.assertEqual(await self.daemon.storage.get_blobs_to_announce(), [])\n        await self.stream_create('foo', '0.01')\n        stream = (await self.daemon.jsonrpc_file_list())[\"items\"][0]\n        self.assertSetEqual(set(await self.daemon.storage.get_blobs_to_announce()), {stream.sd_hash})\n        self.assertTrue(await self.daemon.jsonrpc_file_delete(delete_all=True))\n        # announces on download\n        self.assertEqual(await self.daemon.storage.get_blobs_to_announce(), [])\n        stream = await self.daemon.jsonrpc_get('foo')\n        self.assertSetEqual(set(await self.daemon.storage.get_blobs_to_announce()), {stream.sd_hash})\n\n    async def _purge_file(self, claim_name, full_path):\n        self.assertTrue(\n            await self.daemon.jsonrpc_file_delete(claim_name=claim_name, delete_from_download_dir=True)\n        )\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 0)\n        self.assertFalse(os.path.isfile(full_path))\n\n    async def test_publish_with_illegal_chars(self):\n        def check_prefix_suffix(name, prefix, suffix):\n            self.assertTrue(name.startswith(prefix))\n            self.assertTrue(name.endswith(suffix))\n\n        # Stream a file with file name containing invalid chars\n        claim_name = 'lolwindows'\n        prefix, suffix = 'derp?', '.ext.'\n        san_prefix, san_suffix = 'derp', '.ext'\n        tx = await self.stream_create(claim_name, '0.01', prefix=prefix, suffix=suffix)\n        stream = (await self.daemon.jsonrpc_file_list())[\"items\"][0]\n        claim_id = self.get_claim_id(tx)\n\n        # Assert that file list and source contains the local unsanitized name, but suggested name is sanitized\n        full_path = (await self.daemon.jsonrpc_get('lbry://' + claim_name)).full_path\n        stream_file_name = os.path.basename(full_path)\n        source_file_name = tx['outputs'][0]['value']['source']['name']\n        file_list_name = stream.file_name\n        suggested_file_name = stream.descriptor.suggested_file_name\n\n        self.assertTrue(os.path.isfile(full_path))\n        check_prefix_suffix(stream_file_name, prefix, suffix)\n        self.assertEqual(stream_file_name, source_file_name)\n        self.assertEqual(stream_file_name, file_list_name)\n        check_prefix_suffix(suggested_file_name, san_prefix, san_suffix)\n        await self._purge_file(claim_name, full_path)\n\n        # Re-download deleted file and assert that the file name is sanitized\n        full_path = (await self.daemon.jsonrpc_get('lbry://' + claim_name, save_file=True)).full_path\n        stream_file_name = os.path.basename(full_path)\n        stream = (await self.daemon.jsonrpc_file_list())[\"items\"][0]\n        file_list_name = stream.file_name\n        suggested_file_name = stream.descriptor.suggested_file_name\n\n        self.assertTrue(os.path.isfile(full_path))\n        check_prefix_suffix(stream_file_name, san_prefix, san_suffix)\n        self.assertEqual(stream_file_name, file_list_name)\n        self.assertEqual(stream_file_name, suggested_file_name)\n        await self._purge_file(claim_name, full_path)\n\n        # Assert that the downloaded file name is not sanitized when user provides custom file name\n        custom_name = 'cust*m_name'\n        full_path = (await self.daemon.jsonrpc_get(\n            'lbry://' + claim_name, file_name=custom_name, save_file=True)).full_path\n        file_name_on_disk = os.path.basename(full_path)\n        self.assertTrue(os.path.isfile(full_path))\n        self.assertEqual(custom_name, file_name_on_disk)\n\n        # Update the stream and assert the file name is not sanitized, but the suggested file name is\n        prefix, suffix = 'derpyderp?', '.ext.'\n        san_prefix, san_suffix = 'derpyderp', '.ext'\n        tx = await self.stream_update(claim_id, data=b'amazing content', prefix=prefix, suffix=suffix)\n        full_path = (await self.daemon.jsonrpc_get('lbry://' + claim_name, save_file=True)).full_path\n        updated_stream = (await self.daemon.jsonrpc_file_list())[\"items\"][0]\n\n        stream_file_name = os.path.basename(full_path)\n        source_file_name = tx['outputs'][0]['value']['source']['name']\n        file_list_name = updated_stream.file_name\n        suggested_file_name = updated_stream.descriptor.suggested_file_name\n\n        self.assertTrue(os.path.isfile(full_path))\n        check_prefix_suffix(stream_file_name, prefix, suffix)\n        self.assertEqual(stream_file_name, source_file_name)\n        self.assertEqual(stream_file_name, file_list_name)\n        check_prefix_suffix(suggested_file_name, san_prefix, san_suffix)\n\n    async def test_file_list_fields(self):\n        await self.stream_create('foo', '0.01')\n        file_list = await self.file_list()\n        self.assertEqual(\n            file_list[0]['timestamp'],\n            self.ledger.headers.estimated_timestamp(file_list[0]['height'])\n        )\n        self.assertEqual(file_list[0]['confirmations'], -1)\n        await self.daemon.jsonrpc_resolve('foo')\n        file_list = await self.file_list()\n        self.assertEqual(\n            file_list[0]['timestamp'],\n            self.ledger.headers.estimated_timestamp(file_list[0]['height'])\n        )\n        self.assertEqual(file_list[0]['confirmations'], 1)\n\n    async def test_get_doesnt_touch_user_written_files_between_calls(self):\n        await self.stream_create('foo', '0.01', data=bytes([0] * (2 << 23)))\n        self.assertTrue(await self.daemon.jsonrpc_file_delete(claim_name='foo'))\n        first_path = (await self.daemon.jsonrpc_get('lbry://foo', save_file=True)).full_path\n        await self.wait_files_to_complete()\n        self.assertTrue(await self.daemon.jsonrpc_file_delete(claim_name='foo'))\n        with open(first_path, 'wb') as f:\n            f.write(b' ')\n            f.flush()\n        second_path = await self.daemon.jsonrpc_get('lbry://foo', save_file=True)\n        await self.wait_files_to_complete()\n        self.assertNotEqual(first_path, second_path)\n\n    @unittest.SkipTest  # FIXME: claimname/updateclaim is gone. #3480 wip, unblock #3479\"\n    async def test_file_list_updated_metadata_on_resolve(self):\n        await self.stream_create('foo', '0.01')\n        txo = (await self.daemon.resolve(self.wallet.accounts, ['lbry://foo']))['lbry://foo']\n        claim = txo.claim\n        await self.daemon.jsonrpc_file_delete(claim_name='foo')\n        txid = await self.blockchain_claim_name('bar', hexlify(claim.to_bytes()).decode(), '0.01')\n        await self.daemon.jsonrpc_get('lbry://bar')\n        claim.stream.description = \"fix typos, fix the world\"\n        await self.blockchain_update_name(txid, hexlify(claim.to_bytes()).decode(), '0.01')\n        await self.daemon.jsonrpc_resolve('lbry://bar')\n        file_list = (await self.daemon.jsonrpc_file_list())['items']\n        self.assertEqual(file_list[0].stream_claim_info.claim.stream.description, claim.stream.description)\n\n    async def test_sourceless_content(self):\n        # claim has no source, then it has one\n        tx = await self.stream_create('foo', '0.01', data=None)\n        claim_id = self.get_claim_id(tx)\n        await self.daemon.jsonrpc_file_delete(claim_name='foo')\n        response = await self.out(self.daemon.jsonrpc_get('lbry://foo'))\n        self.assertIn('error', response)\n        self.assertIn('nothing to download', response['error'])\n        # source is set (there isn't a way to clear the source field, so we stop here for now)\n        await self.stream_update(claim_id, data=b'surpriiiiiiiise')\n        response = await self.out(self.daemon.jsonrpc_get('lbry://foo'))\n        self.assertNotIn('error', response)\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 1)\n\n    async def test_file_list_paginated_output(self):\n        await self.create_streams_in_range(0, 20)\n\n        page = await self.file_list(page_size=20)\n        page_claim_ids = [item['claim_id'] for item in page]\n        self.assertListEqual(page_claim_ids, self.stream_claim_ids)\n\n        page = await self.file_list(page_size=6)\n        page_claim_ids = [item['claim_id'] for item in page]\n        self.assertListEqual(page_claim_ids, self.stream_claim_ids[:6])\n\n        page = await self.file_list(page_size=6, page=2)\n        page_claim_ids = [item['claim_id'] for item in page]\n        self.assertListEqual(page_claim_ids, self.stream_claim_ids[6:12])\n\n        out_of_bounds = await self.file_list(page=5, page_size=6)\n        self.assertEqual(out_of_bounds, [])\n\n        complete = await self.daemon.jsonrpc_file_list()\n        self.assertEqual(complete['total_pages'], 1)\n        self.assertEqual(complete['total_items'], 20)\n\n        page = await self.daemon.jsonrpc_file_list(page_size=10, page=1)\n        self.assertEqual(page['total_pages'], 2)\n        self.assertEqual(page['total_items'], 20)\n        self.assertEqual(page['page'], 1)\n\n        full = await self.out(self.daemon.jsonrpc_file_list(page_size=20, page=1))\n        page1 = await self.file_list(page=1, page_size=10)\n        page2 = await self.file_list(page=2, page_size=10)\n        self.assertEqual(page1 + page2, full['items'])\n\n    async def test_download_different_timeouts(self):\n        tx = await self.stream_create('foo', '0.01')\n        sd_hash = tx['outputs'][0]['value']['source']['sd_hash']\n        await self.daemon.jsonrpc_file_delete(claim_name='foo')\n        all_except_sd = [\n            blob_hash for blob_hash in self.server.blob_manager.completed_blob_hashes if blob_hash != sd_hash\n        ]\n        await self.server.blob_manager.delete_blobs(all_except_sd)\n        resp = await self.daemon.jsonrpc_get('lbry://foo', timeout=2, save_file=True)\n        self.assertIn('error', resp)\n        self.assertEqual('Failed to download data blobs for sd hash %s within timeout.' % sd_hash, resp['error'])\n        self.assertTrue(await self.daemon.jsonrpc_file_delete(claim_name='foo'), \"data timeout didn't create a file\")\n        await self.server.blob_manager.delete_blobs([sd_hash])\n        resp = await self.daemon.jsonrpc_get('lbry://foo', timeout=2, save_file=True)\n        self.assertIn('error', resp)\n        self.assertEqual('Failed to download sd blob %s within timeout.' % sd_hash, resp['error'])\n\n    async def wait_files_to_complete(self):\n        while await self.file_list(status='running'):\n            await asyncio.sleep(0.01)\n\n    async def test_filename_conflicts_management_on_resume_download(self):\n        await self.stream_create('foo', '0.01', data=bytes([0] * (1 << 23)))\n        file_info = (await self.file_list())[0]\n        original_path = os.path.join(self.daemon.conf.download_dir, file_info['file_name'])\n        await self.daemon.jsonrpc_file_delete(claim_name='foo')\n        await self.daemon.jsonrpc_get('lbry://foo')\n        with open(original_path, 'wb') as handle:\n            handle.write(b'some other stuff was there instead')\n        await self.daemon.file_manager.stop()\n        await self.daemon.file_manager.start()\n        await asyncio.wait_for(self.wait_files_to_complete(), timeout=5)  # if this hangs, file didn't get set completed\n        # check that internal state got through up to the file list API\n        stream = self.daemon.file_manager.get_filtered(stream_hash=file_info['stream_hash'])[0]\n        file_info = (await self.file_list())[0]\n        self.assertEqual(stream.file_name, file_info['file_name'])\n        # checks if what the API shows is what he have at the very internal level.\n        self.assertEqual(stream.full_path, file_info['download_path'])\n\n    async def test_incomplete_downloads_erases_output_file_on_stop(self):\n        tx = await self.stream_create('foo', '0.01', data=b'deadbeef' * 1000000)\n        sd_hash = tx['outputs'][0]['value']['source']['sd_hash']\n        file_info = (await self.file_list())[0]\n        blobs = await self.daemon.storage.get_blobs_for_stream(\n            await self.daemon.storage.get_stream_hash_for_sd_hash(sd_hash)\n        )\n        await self.daemon.jsonrpc_file_delete(claim_name='foo')\n        self.assertEqual(5, len(blobs))\n        all_except_sd_and_head = [\n            blob.blob_hash for blob in blobs[1:-1]\n        ]\n        await self.server.blob_manager.delete_blobs(all_except_sd_and_head)\n        path = os.path.join(self.daemon.conf.download_dir, file_info['file_name'])\n        self.assertFalse(os.path.isfile(path))\n        resp = await self.out(self.daemon.jsonrpc_get('lbry://foo', timeout=2))\n        self.assertNotIn('error', resp)\n        self.assertTrue(os.path.isfile(path))\n        await self.daemon.file_manager.stop()\n        self.assertFalse(os.path.isfile(path))\n\n    async def test_incomplete_downloads_retry(self):\n        tx = await self.stream_create('foo', '0.01', data=b'deadbeef' * 1000000)\n        sd_hash = tx['outputs'][0]['value']['source']['sd_hash']\n        blobs = await self.daemon.storage.get_blobs_for_stream(\n            await self.daemon.storage.get_stream_hash_for_sd_hash(sd_hash)\n        )\n        self.assertEqual(5, len(blobs))\n        await self.daemon.jsonrpc_file_delete(claim_name='foo')\n        all_except_sd_and_head = [\n            blob.blob_hash for blob in blobs[1:-1]\n        ]\n\n        # backup server blobs\n        for blob_hash in all_except_sd_and_head:\n            blob = self.server_blob_manager.get_blob(blob_hash)\n            os.rename(blob.file_path, blob.file_path + '__')\n\n        # erase all except sd blob\n        await self.server.blob_manager.delete_blobs(all_except_sd_and_head)\n\n        # start the download\n        resp = await self.out(self.daemon.jsonrpc_get('lbry://foo', timeout=2))\n        self.assertNotIn('error', resp)\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 1)\n        self.assertEqual('running', (await self.file_list())[0]['status'])\n\n        # recover blobs\n        for blob_hash in all_except_sd_and_head:\n            blob = self.server_blob_manager.get_blob(blob_hash)\n            os.rename(blob.file_path + '__', blob.file_path)\n            self.server_blob_manager.blobs.clear()\n            await self.server_blob_manager.blob_completed(self.server_blob_manager.get_blob(blob_hash))\n\n        await asyncio.wait_for(self.wait_files_to_complete(), timeout=5)\n        file_info = (await self.file_list())[0]\n        self.assertEqual(file_info['blobs_completed'], file_info['blobs_in_stream'])\n        self.assertEqual('finished', file_info['status'])\n\n    async def test_paid_download(self):\n        target_address = await self.blockchain.get_raw_change_address()\n\n        # FAIL: beyond available balance\n        await self.stream_create(\n            'expensive', '0.01', data=b'pay me if you can',\n            fee_currency='LBC', fee_amount='11.0',\n            fee_address=target_address, claim_address=target_address\n        )\n        await self.daemon.jsonrpc_file_delete(claim_name='expensive')\n        response = await self.out(self.daemon.jsonrpc_get('lbry://expensive'))\n        self.assertEqual(response['error'], 'Not enough funds to cover this transaction.')\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 0)\n\n        # FAIL: beyond maximum key fee\n        await self.stream_create(\n            'maxkey', '0.01', data=b'no pay me, no',\n            fee_currency='LBC', fee_amount='111.0',\n            fee_address=target_address, claim_address=target_address\n        )\n        await self.daemon.jsonrpc_file_delete(claim_name='maxkey')\n        response = await self.out(self.daemon.jsonrpc_get('lbry://maxkey'))\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 0)\n        self.assertEqual(\n            response['error'], 'Purchase price of 111.0 LBC exceeds maximum configured price of 100.0 LBC (50.0 USD).'\n        )\n\n        # PASS: purchase is successful\n        await self.stream_create(\n            'icanpay', '0.01', data=b'I got the power!',\n            fee_currency='LBC', fee_amount='1.0',\n            fee_address=target_address, claim_address=target_address\n        )\n        await self.daemon.jsonrpc_file_delete(claim_name='icanpay')\n        await self.assertBalance(self.account, '9.925679')\n        response = await self.daemon.jsonrpc_get('lbry://icanpay')\n        raw_content_fee = response.content_fee.raw\n        await self.ledger.wait(response.content_fee)\n        await self.assertBalance(self.account, '8.925538')\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 1)\n\n        await asyncio.wait_for(self.wait_files_to_complete(), timeout=1)\n\n        # check that the fee was received\n        starting_balance = float(await self.blockchain.get_balance())\n        await self.generate(1)\n        block_reward_and_claim_fee = 2.0\n        self.assertEqual(\n            float(await self.blockchain.get_balance()),\n            starting_balance + block_reward_and_claim_fee\n        )\n\n        # restart the daemon and make sure the fee is still there\n\n        await self.daemon.file_manager.stop()\n        await self.daemon.file_manager.start()\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 1)\n        self.assertEqual((await self.daemon.jsonrpc_file_list())['items'][0].content_fee.raw, raw_content_fee)\n        await self.daemon.jsonrpc_file_delete(claim_name='icanpay')\n\n        # PASS: no fee address --> use the claim address to pay\n        tx = await self.stream_create(\n            'nofeeaddress', '0.01', data=b'free stuff?',\n        )\n        await self.__raw_value_update_no_fee_address(\n            tx, fee_amount='2.0', fee_currency='LBC', claim_address=target_address\n        )\n        await self.daemon.jsonrpc_file_delete(claim_name='nofeeaddress')\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 0)\n\n        response = await self.out(self.daemon.jsonrpc_get('lbry://nofeeaddress'))\n        self.assertIsNone((await self.daemon.jsonrpc_file_list())['items'][0].stream_claim_info.claim.stream.fee.address)\n        self.assertIsNotNone(response['content_fee'])\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 1)\n        self.assertEqual(response['content_fee']['outputs'][0]['amount'], '2.0')\n        self.assertEqual(response['content_fee']['outputs'][0]['address'], target_address)\n\n    async def test_null_max_key_fee(self):\n        target_address = await self.blockchain.get_raw_change_address()\n        self.daemon.conf.max_key_fee = None\n\n        await self.stream_create(\n            'somename', '0.5', data=b'Yes, please',\n            fee_currency='LBC', fee_amount='1.0',\n            fee_address=target_address, claim_address=target_address\n        )\n        self.assertTrue(await self.daemon.jsonrpc_file_delete(claim_name='somename'))\n        # Assert the fee and bid are subtracted\n        await self.assertBalance(self.account, '9.483893')\n        response = await self.daemon.jsonrpc_get('lbry://somename')\n        await self.ledger.wait(response.content_fee)\n        await self.assertBalance(self.account, '8.483752')\n\n        # Assert the file downloads\n        await asyncio.wait_for(self.wait_files_to_complete(), timeout=1)\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 1)\n\n        # Assert the transaction is recorded to the blockchain\n        starting_balance = float(await self.blockchain.get_balance())\n        await self.generate(1)\n        block_reward_and_claim_fee = 2.0\n        self.assertEqual(\n            float(await self.blockchain.get_balance()), starting_balance + block_reward_and_claim_fee\n        )\n\n    async def test_null_fee(self):\n        target_address = await self.blockchain.get_raw_change_address()\n        tx = await self.stream_create(\n            'nullfee', '0.01', data=b'no pay me, no',\n            fee_currency='LBC', fee_address=target_address, fee_amount='1.0'\n        )\n        await self.__raw_value_update_no_fee_amount(tx, target_address)\n        await self.daemon.jsonrpc_file_delete(claim_name='nullfee')\n        response = await self.daemon.jsonrpc_get('lbry://nullfee')\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 1)\n        self.assertIsNone(response.content_fee)\n        self.assertTrue(response.stream_claim_info.claim.stream.has_fee)\n        self.assertDictEqual(\n            response.stream_claim_info.claim.stream.to_dict()['fee'],\n            {'currency': 'LBC', 'address': target_address}\n        )\n        await self.daemon.jsonrpc_file_delete(claim_name='nullfee')\n\n    async def __raw_value_update_no_fee_address(self, tx, claim_address, **kwargs):\n        tx = await self.daemon.jsonrpc_stream_update(\n            self.get_claim_id(tx), preview=True, claim_address=claim_address, **kwargs\n        )\n        tx.outputs[0].claim.stream.fee.address_bytes = b''\n        tx.outputs[0].script.generate()\n        await tx.sign([self.account])\n        await self.broadcast_and_confirm(tx)\n\n    async def __raw_value_update_no_fee_amount(self, tx, claim_address):\n        tx = await self.daemon.jsonrpc_stream_update(\n            self.get_claim_id(tx), preview=True, fee_currency='LBC', fee_amount='1.0', fee_address=claim_address,\n            claim_address=claim_address\n        )\n        tx.outputs[0].claim.stream.fee.message.ClearField('amount')\n        tx.outputs[0].script.generate()\n        await tx.sign([self.account])\n        await self.broadcast_and_confirm(tx)\n\n\nclass DiskSpaceManagement(CommandTestCase):\n\n    async def get_referenced_blobs(self, tx):\n        sd_hash = tx['outputs'][0]['value']['source']['sd_hash']\n        stream_hash = await self.daemon.storage.get_stream_hash_for_sd_hash(sd_hash)\n        return tx['outputs'][0]['value']['source']['sd_hash'], set(await self.blob_list(\n            stream_hash=stream_hash\n        ))\n\n    async def test_file_management(self):\n        status = await self.status()\n        self.assertIn('disk_space', status)\n        self.assertEqual(0, status['disk_space']['total_used_mb'])\n        self.assertEqual(True, status['disk_space']['running'])\n        sd_hash1, blobs1 = await self.get_referenced_blobs(\n            await self.stream_create('foo1', '0.01', data=('0' * 2 * 1024 * 1024).encode())\n        )\n        sd_hash2, blobs2 = await self.get_referenced_blobs(\n            await self.stream_create('foo2', '0.01', data=('0' * 3 * 1024 * 1024).encode())\n        )\n        sd_hash3, blobs3 = await self.get_referenced_blobs(\n            await self.stream_create('foo3', '0.01', data=('0' * 3 * 1024 * 1024).encode())\n        )\n        sd_hash4, blobs4 = await self.get_referenced_blobs(\n            await self.stream_create('foo4', '0.01', data=('0' * 2 * 1024 * 1024).encode())\n        )\n\n        await self.daemon.storage.update_blob_ownership(sd_hash1, False)\n        await self.daemon.storage.update_blob_ownership(sd_hash3, False)\n        await self.daemon.storage.update_blob_ownership(sd_hash4, False)\n        await self.blob_clean()  # just to refresh caches, has no effect\n\n        self.assertEqual(7, (await self.status())['disk_space']['content_blobs_storage_used_mb'])\n        self.assertEqual(10, (await self.status())['disk_space']['total_used_mb'])\n        self.assertEqual(blobs1 | blobs2 | blobs3 | blobs4, set(await self.blob_list()))\n\n        await self.blob_clean()\n\n        self.assertEqual(10, (await self.status())['disk_space']['total_used_mb'])\n        self.assertEqual(7, (await self.status())['disk_space']['content_blobs_storage_used_mb'])\n        self.assertEqual(3, (await self.status())['disk_space']['published_blobs_storage_used_mb'])\n        self.assertEqual(blobs1 | blobs2 | blobs3 | blobs4, set(await self.blob_list()))\n\n        self.daemon.conf.blob_storage_limit = 6\n        await self.blob_clean()\n\n        self.assertEqual(5, (await self.status())['disk_space']['total_used_mb'])\n        self.assertEqual(2, (await self.status())['disk_space']['content_blobs_storage_used_mb'])\n        self.assertEqual(3, (await self.status())['disk_space']['published_blobs_storage_used_mb'])\n        blobs = set(await self.blob_list())\n        self.assertFalse(blobs1.issubset(blobs))\n        self.assertTrue(blobs2.issubset(blobs))\n        self.assertFalse(blobs3.issubset(blobs))\n        self.assertTrue(blobs4.issubset(blobs))\n        # check that pending blobs are not accounted (#3617)\n        await self.daemon.storage.db.execute_fetchall(\"update blob set status='pending'\")\n        await self.blob_clean()  # just to refresh caches, has no effect\n        self.assertEqual(0, (await self.status())['disk_space']['total_used_mb'])\n        self.assertEqual(0, (await self.status())['disk_space']['content_blobs_storage_used_mb'])\n        self.assertEqual(0, (await self.status())['disk_space']['published_blobs_storage_used_mb'])\n        # check that added_on gets set on downloads (was a bug)\n        self.assertLess(0, await self.daemon.storage.run_and_return_one_or_none(\"select min(added_on) from blob\"))\n        await self.daemon.jsonrpc_file_delete(delete_all=True)\n        await self.daemon.jsonrpc_get(\"foo4\", save_file=False)\n        self.assertLess(0, await self.daemon.storage.run_and_return_one_or_none(\"select min(added_on) from blob\"))\n\nclass TestBackgroundDownloaderComponent(CommandTestCase):\n    async def get_blobs_from_sd_blob(self, sd_blob):\n        descriptor = await StreamDescriptor.from_stream_descriptor_blob(\n            asyncio.get_running_loop(), self.daemon.blob_manager.blob_dir, sd_blob\n        )\n        return descriptor.blobs\n\n    async def assertBlobs(self, *sd_hashes, no_files=True):\n        # checks that we have ony the finished blobs needed for the the referenced streams\n        seen = set(sd_hashes)\n        for sd_hash in sd_hashes:\n            sd_blob = self.daemon.blob_manager.get_blob(sd_hash)\n            self.assertTrue(sd_blob.get_is_verified())\n            blobs = await self.get_blobs_from_sd_blob(sd_blob)\n            for blob in blobs[:-1]:\n                self.assertTrue(self.daemon.blob_manager.get_blob(blob.blob_hash).get_is_verified())\n            seen.update(blob.blob_hash for blob in blobs if blob.blob_hash)\n        if no_files:\n            self.assertEqual(seen, self.daemon.blob_manager.completed_blob_hashes)\n            self.assertEqual(0, len(await self.file_list()))\n\n    async def clear(self):\n        await self.daemon.jsonrpc_file_delete(delete_all=True)\n        self.assertEqual(0, len(await self.file_list()))\n        await self.daemon.blob_manager.delete_blobs(list(self.daemon.blob_manager.completed_blob_hashes), True)\n        self.assertEqual(0, len((await self.daemon.jsonrpc_blob_list())['items']))\n\n    async def test_download(self):\n        content1 = await self.stream_create('content1', '0.01', data=bytes([0] * 32 * 1024 * 1024))\n        content1 = content1['outputs'][0]['value']['source']['sd_hash']\n        content2 = await self.stream_create('content2', '0.01', data=bytes([0] * 16 * 1024 * 1024))\n        content2 = content2['outputs'][0]['value']['source']['sd_hash']\n        self.assertEqual(48, (await self.status())['disk_space']['published_blobs_storage_used_mb'])\n        self.assertEqual(0, (await self.status())['disk_space']['content_blobs_storage_used_mb'])\n\n        background_downloader = BackgroundDownloader(self.daemon.conf, self.daemon.storage, self.daemon.blob_manager)\n        self.daemon.conf.network_storage_limit = 32\n        await self.clear()\n        await self.blob_clean()\n        self.assertEqual(0, (await self.status())['disk_space']['total_used_mb'])\n        await background_downloader.download_blobs(content1)\n        await self.assertBlobs(content1)\n        await self.blob_clean()\n        self.assertEqual(0, (await self.status())['disk_space']['content_blobs_storage_used_mb'])\n        self.assertEqual(32, (await self.status())['disk_space']['seed_blobs_storage_used_mb'])\n        self.daemon.conf.network_storage_limit = 48\n        await background_downloader.download_blobs(content2)\n        await self.assertBlobs(content1, content2)\n        await self.blob_clean()\n        self.assertEqual(0, (await self.status())['disk_space']['content_blobs_storage_used_mb'])\n        self.assertEqual(48, (await self.status())['disk_space']['seed_blobs_storage_used_mb'])\n        await self.clear()\n        await background_downloader.download_blobs(content2)\n        await self.assertBlobs(content2)\n        await self.blob_clean()\n        self.assertEqual(0, (await self.status())['disk_space']['content_blobs_storage_used_mb'])\n        self.assertEqual(16, (await self.status())['disk_space']['seed_blobs_storage_used_mb'])\n\n        # tests that an attempt to download something that isn't a sd blob will download the single blob and stop\n        blobs = await self.get_blobs_from_sd_blob(self.reflector.blob_manager.get_blob(content1))\n        await self.clear()\n        await background_downloader.download_blobs(blobs[0].blob_hash)\n        self.assertEqual({blobs[0].blob_hash}, self.daemon.blob_manager.completed_blob_hashes)\n\n        # test that disk space manager doesn't delete orphan network blobs\n        await background_downloader.download_blobs(content1)\n        await self.daemon.storage.db.execute_fetchall(\"update blob set added_on=0\")  # so it is preferred for cleaning\n        await self.daemon.jsonrpc_get(\"content2\", save_file=False)\n        while (await self.file_list())[0]['status'] != 'stopped':\n            await asyncio.sleep(0.5)\n        await self.assertBlobs(content1, no_files=False)\n\n        self.daemon.conf.blob_storage_limit = 1\n        await self.blob_clean()\n        await self.assertBlobs(content1, no_files=False)\n\n        self.daemon.conf.network_storage_limit = 0\n        await self.blob_clean()\n        self.assertEqual(0, (await self.status())['disk_space']['seed_blobs_storage_used_mb'])\n"
  },
  {
    "path": "tests/integration/datanetwork/test_streaming.py",
    "content": "import os\nimport hashlib\nimport aiohttp\nimport aiohttp.web\nimport asyncio\nimport contextlib\n\nfrom lbry.file.source import ManagedDownloadSource\nfrom lbry.utils import aiohttp_request\nfrom lbry.blob.blob_file import MAX_BLOB_SIZE\nfrom lbry.testcase import CommandTestCase\n\n\ndef get_random_bytes(n: int) -> bytes:\n    result = b''.join(hashlib.sha256(os.urandom(4)).digest() for _ in range(n // 16))\n    if len(result) < n:\n        result += os.urandom(n - len(result))\n    elif len(result) > n:\n        result = result[:-(len(result) - n)]\n    assert len(result) == n, (n, len(result))\n    return result\n\n\nclass RangeRequests(CommandTestCase):\n    async def _restart_stream_manager(self):\n        await self.daemon.file_manager.stop()\n        await self.daemon.file_manager.start()\n        return\n\n    async def _setup_stream(self, data: bytes, save_blobs: bool = True, save_files: bool = False, file_size=0):\n        self.daemon.conf.save_blobs = save_blobs\n        self.daemon.conf.save_files = save_files\n        self.data = data\n        await self.stream_create('foo', '0.01', data=self.data, file_size=file_size)\n        if save_blobs:\n            self.assertGreater(len(os.listdir(self.daemon.blob_manager.blob_dir)), 1)\n        await (await self.daemon.jsonrpc_file_list())['items'][0].fully_reflected.wait()\n        await self.daemon.jsonrpc_file_delete(delete_from_download_dir=True, claim_name='foo')\n        self.assertEqual(0, len(os.listdir(self.daemon.blob_manager.blob_dir)))\n        # await self._restart_stream_manager()\n        await self.daemon.streaming_runner.setup()\n        site = aiohttp.web.TCPSite(self.daemon.streaming_runner, self.daemon.conf.streaming_host,\n                                   self.daemon.conf.streaming_port)\n        await site.start()\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 0)\n\n    async def _test_range_requests(self):\n        name = 'foo'\n        url = f'http://{self.daemon.conf.streaming_host}:{self.daemon.conf.streaming_port}/get/{name}'\n\n        async with aiohttp_request('get', url) as req:\n            self.assertEqual(req.headers.get('Content-Type'), 'application/octet-stream')\n            content_range = req.headers.get('Content-Range')\n            content_length = int(req.headers.get('Content-Length'))\n            streamed_bytes = await req.content.read()\n        self.assertEqual(content_length, len(streamed_bytes))\n        return streamed_bytes, content_range, content_length\n\n    async def test_range_requests_2_byte(self):\n        self.data = b'hi'\n        await self._setup_stream(self.data)\n        streamed, content_range, content_length = await self._test_range_requests()\n        self.assertEqual(15, content_length)\n        self.assertEqual(b'hi\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00', streamed)\n        self.assertEqual('bytes 0-14/15', content_range)\n\n    async def test_range_requests_15_byte(self):\n        self.data = b'123456789abcdef'\n        await self._setup_stream(self.data)\n        streamed, content_range, content_length = await self._test_range_requests()\n        self.assertEqual(15, content_length)\n        self.assertEqual(15, len(streamed))\n        self.assertEqual(self.data, streamed)\n        self.assertEqual('bytes 0-14/15', content_range)\n\n    async def test_range_requests_0_padded_bytes(self, size: int = (MAX_BLOB_SIZE - 1) * 4,\n                                                 expected_range: str = 'bytes 0-8388603/8388604', padding=b'',\n                                                 file_size=0):\n        self.data = get_random_bytes(size)\n        await self._setup_stream(self.data, file_size=file_size)\n        streamed, content_range, content_length = await self._test_range_requests()\n        self.assertEqual(len(self.data + padding), content_length)\n        self.assertEqual(streamed, self.data + padding)\n        self.assertEqual(expected_range, content_range)\n\n    async def test_range_requests_1_padded_bytes(self):\n        await self.test_range_requests_0_padded_bytes(\n            ((MAX_BLOB_SIZE - 1) * 4) - 1, padding=b'\\x00'\n        )\n\n    async def test_range_requests_2_padded_bytes(self):\n        await self.test_range_requests_0_padded_bytes(\n            ((MAX_BLOB_SIZE - 1) * 4) - 2, padding=b'\\x00' * 2\n        )\n\n    async def test_range_requests_14_padded_bytes(self):\n        await self.test_range_requests_0_padded_bytes(\n            ((MAX_BLOB_SIZE - 1) * 4) - 14, padding=b'\\x00' * 14\n        )\n\n    async def test_range_requests_no_padding_size_from_claim(self):\n        size = ((MAX_BLOB_SIZE - 1) * 4) - 14\n        await self.test_range_requests_0_padded_bytes(size, padding=b'', file_size=size,\n                                                      expected_range=f\"bytes 0-{size-1}/{size}\")\n\n    async def test_range_requests_15_padded_bytes(self):\n        await self.test_range_requests_0_padded_bytes(\n            ((MAX_BLOB_SIZE - 1) * 4) - 15, padding=b'\\x00' * 15\n        )\n\n    async def test_forbidden(self):\n        self.data = get_random_bytes(1000)\n        await self._setup_stream(self.data, file_size=1000)\n        url = f'http://{self.daemon.conf.streaming_host}:{self.daemon.conf.streaming_port}/get/foo'\n        self.daemon.conf.streaming_get = False\n        async with aiohttp_request('get', url) as req:\n            self.assertEqual(403, req.status)\n\n    async def test_range_requests_last_block_of_last_blob_padding(self):\n        self.data = get_random_bytes(((MAX_BLOB_SIZE - 1) * 4) - 16)\n        await self._setup_stream(self.data)\n        streamed, content_range, content_length = await self._test_range_requests()\n        self.assertEqual(len(self.data), content_length)\n        self.assertEqual(streamed, self.data)\n        self.assertEqual('bytes 0-8388587/8388588', content_range)\n\n    async def test_streaming_only_with_blobs(self):\n        self.data = get_random_bytes((MAX_BLOB_SIZE - 1) * 4)\n        await self._setup_stream(self.data)\n\n        await self._test_range_requests()\n        stream = (await self.daemon.jsonrpc_file_list())['items'][0]\n        self.assertTrue(os.path.isfile(self.daemon.blob_manager.get_blob(stream.sd_hash).file_path))\n        self.assertIsNone(stream.download_directory)\n        self.assertIsNone(stream.full_path)\n        files_in_download_dir = list(os.scandir(self.daemon.conf.data_dir))\n\n        # test that repeated range requests do not create duplicate files\n        for _ in range(3):\n            await self._test_range_requests()\n            stream = (await self.daemon.jsonrpc_file_list())['items'][0]\n            self.assertTrue(os.path.isfile(self.daemon.blob_manager.get_blob(stream.sd_hash).file_path))\n            self.assertIsNone(stream.download_directory)\n            self.assertIsNone(stream.full_path)\n            current_files_in_download_dir = list(os.scandir(self.daemon.conf.data_dir))\n            self.assertEqual(\n                len(files_in_download_dir), len(current_files_in_download_dir)\n            )\n\n        # test that a range request after restart does not create a duplicate file\n        await self._restart_stream_manager()\n\n        current_files_in_download_dir = list(os.scandir(self.daemon.conf.data_dir))\n        self.assertEqual(\n            len(files_in_download_dir), len(current_files_in_download_dir)\n        )\n        stream = (await self.daemon.jsonrpc_file_list())['items'][0]\n        self.assertTrue(os.path.isfile(self.daemon.blob_manager.get_blob(stream.sd_hash).file_path))\n        self.assertIsNone(stream.download_directory)\n        self.assertIsNone(stream.full_path)\n\n        await self._test_range_requests()\n        stream = (await self.daemon.jsonrpc_file_list())['items'][0]\n        self.assertTrue(os.path.isfile(self.daemon.blob_manager.get_blob(stream.sd_hash).file_path))\n        self.assertIsNone(stream.download_directory)\n        self.assertIsNone(stream.full_path)\n        current_files_in_download_dir = list(os.scandir(self.daemon.conf.data_dir))\n        self.assertEqual(\n            len(files_in_download_dir), len(current_files_in_download_dir)\n        )\n\n    async def test_streaming_only_without_blobs(self):\n        self.data = get_random_bytes((MAX_BLOB_SIZE - 1) * 4)\n        await self._setup_stream(self.data, save_blobs=False)\n        await self._test_range_requests()\n        stream = (await self.daemon.jsonrpc_file_list())['items'][0]\n        self.assertIsNone(stream.download_directory)\n        self.assertIsNone(stream.full_path)\n        files_in_download_dir = list(os.scandir(self.daemon.conf.data_dir))\n\n        # test that repeated range requests do not create duplicate files\n        for _ in range(3):\n            await self._test_range_requests()\n            stream = (await self.daemon.jsonrpc_file_list())['items'][0]\n            self.assertIsNone(stream.download_directory)\n            self.assertIsNone(stream.full_path)\n            current_files_in_download_dir = list(os.scandir(self.daemon.conf.data_dir))\n            self.assertEqual(\n                len(files_in_download_dir), len(current_files_in_download_dir)\n            )\n\n        # test that a range request after restart does not create a duplicate file\n        await self._restart_stream_manager()\n\n        current_files_in_download_dir = list(os.scandir(self.daemon.conf.data_dir))\n        self.assertEqual(\n            len(files_in_download_dir), len(current_files_in_download_dir)\n        )\n        stream = (await self.daemon.jsonrpc_file_list())['items'][0]\n        self.assertIsNone(stream.download_directory)\n        self.assertIsNone(stream.full_path)\n\n        await self._test_range_requests()\n        stream = (await self.daemon.jsonrpc_file_list())['items'][0]\n        self.assertIsNone(stream.download_directory)\n        self.assertIsNone(stream.full_path)\n        current_files_in_download_dir = list(os.scandir(self.daemon.conf.data_dir))\n        self.assertEqual(\n            len(files_in_download_dir), len(current_files_in_download_dir)\n        )\n\n    async def test_stream_and_save_file_with_blobs(self):\n        self.data = get_random_bytes((MAX_BLOB_SIZE - 1) * 4)\n        await self._setup_stream(self.data, save_files=True)\n\n        await self._test_range_requests()\n        streams = (await self.daemon.jsonrpc_file_list())['items']\n        self.assertEqual(1, len(streams))\n        stream = streams[0]\n        self.assertTrue(os.path.isfile(self.daemon.blob_manager.get_blob(stream.sd_hash).file_path))\n        self.assertTrue(os.path.isdir(stream.download_directory))\n        self.assertTrue(os.path.isfile(stream.full_path))\n        full_path = stream.full_path\n        files_in_download_dir = list(os.scandir(os.path.dirname(full_path)))\n\n        for _ in range(3):\n            await self._test_range_requests()\n            streams = (await self.daemon.jsonrpc_file_list())['items']\n            self.assertEqual(1, len(streams))\n            stream = streams[0]\n            self.assertTrue(os.path.isfile(self.daemon.blob_manager.get_blob(stream.sd_hash).file_path))\n            self.assertTrue(os.path.isdir(stream.download_directory))\n            self.assertTrue(os.path.isfile(stream.full_path))\n            current_files_in_download_dir = list(os.scandir(os.path.dirname(full_path)))\n            self.assertEqual(\n                len(files_in_download_dir), len(current_files_in_download_dir)\n            )\n\n        await self._restart_stream_manager()\n\n        current_files_in_download_dir = list(os.scandir(os.path.dirname(full_path)))\n        self.assertEqual(\n            len(files_in_download_dir), len(current_files_in_download_dir)\n        )\n        streams = (await self.daemon.jsonrpc_file_list())['items']\n        self.assertEqual(1, len(streams))\n        stream = streams[0]\n        self.assertTrue(os.path.isfile(self.daemon.blob_manager.get_blob(stream.sd_hash).file_path))\n        self.assertTrue(os.path.isdir(stream.download_directory))\n        self.assertTrue(os.path.isfile(stream.full_path))\n\n        await self._test_range_requests()\n        streams = (await self.daemon.jsonrpc_file_list())['items']\n        self.assertEqual(1, len(streams))\n        stream = streams[0]\n        self.assertTrue(os.path.isfile(self.daemon.blob_manager.get_blob(stream.sd_hash).file_path))\n        self.assertTrue(os.path.isdir(stream.download_directory))\n        self.assertTrue(os.path.isfile(stream.full_path))\n        current_files_in_download_dir = list(os.scandir(os.path.dirname(full_path)))\n        self.assertEqual(\n            len(files_in_download_dir), len(current_files_in_download_dir)\n        )\n        with open(stream.full_path, 'rb') as f:\n            self.assertEqual(self.data, f.read())\n\n    async def test_stream_and_save_file_without_blobs(self):\n        self.data = get_random_bytes((MAX_BLOB_SIZE - 1) * 4)\n        await self._setup_stream(self.data, save_files=True)\n        self.daemon.conf.save_blobs = False\n\n        await self._test_range_requests()\n        stream = (await self.daemon.jsonrpc_file_list())['items'][0]\n        self.assertTrue(os.path.isdir(stream.download_directory))\n        self.assertTrue(os.path.isfile(stream.full_path))\n        full_path = stream.full_path\n        files_in_download_dir = list(os.scandir(os.path.dirname(full_path)))\n\n        for _ in range(3):\n            await self._test_range_requests()\n            stream = (await self.daemon.jsonrpc_file_list())['items'][0]\n            self.assertTrue(os.path.isdir(stream.download_directory))\n            self.assertTrue(os.path.isfile(stream.full_path))\n            current_files_in_download_dir = list(os.scandir(os.path.dirname(full_path)))\n            self.assertEqual(\n                len(files_in_download_dir), len(current_files_in_download_dir)\n            )\n\n        await self._restart_stream_manager()\n        current_files_in_download_dir = list(os.scandir(os.path.dirname(full_path)))\n        self.assertEqual(\n            len(files_in_download_dir), len(current_files_in_download_dir)\n        )\n        streams = (await self.daemon.jsonrpc_file_list())['items']\n        self.assertEqual(1, len(streams))\n        stream = streams[0]\n        self.assertTrue(os.path.isdir(stream.download_directory))\n        self.assertTrue(os.path.isfile(stream.full_path))\n\n        await self._test_range_requests()\n        streams = (await self.daemon.jsonrpc_file_list())['items']\n        self.assertEqual(1, len(streams))\n        stream = streams[0]\n        self.assertTrue(os.path.isdir(stream.download_directory))\n        self.assertTrue(os.path.isfile(stream.full_path))\n        current_files_in_download_dir = list(os.scandir(os.path.dirname(full_path)))\n        self.assertEqual(\n            len(files_in_download_dir), len(current_files_in_download_dir)\n        )\n\n        with open(stream.full_path, 'rb') as f:\n            self.assertEqual(self.data, f.read())\n\n    async def test_switch_save_blobs_while_running(self):\n        await self.test_streaming_only_without_blobs()\n        self.daemon.conf.save_blobs = True\n        blobs_in_stream = (await self.daemon.jsonrpc_file_list())['items'][0].blobs_in_stream\n        sd_hash = (await self.daemon.jsonrpc_file_list())['items'][0].sd_hash\n        start_file_count = len(os.listdir(self.daemon.blob_manager.blob_dir))\n        await self._test_range_requests()\n        self.assertEqual(start_file_count + blobs_in_stream, len(os.listdir(self.daemon.blob_manager.blob_dir)))\n        self.assertEqual(0, (await self.daemon.jsonrpc_file_list())['items'][0].blobs_remaining)\n\n        # switch back\n        self.daemon.conf.save_blobs = False\n        await self._test_range_requests()\n        self.assertEqual(start_file_count + blobs_in_stream, len(os.listdir(self.daemon.blob_manager.blob_dir)))\n        self.assertEqual(0, (await self.daemon.jsonrpc_file_list())['items'][0].blobs_remaining)\n        await self.daemon.jsonrpc_file_delete(delete_from_download_dir=True, sd_hash=sd_hash)\n        self.assertEqual(start_file_count, len(os.listdir(self.daemon.blob_manager.blob_dir)))\n        await self._test_range_requests()\n        self.assertEqual(start_file_count, len(os.listdir(self.daemon.blob_manager.blob_dir)))\n        self.assertEqual(blobs_in_stream, (await self.daemon.jsonrpc_file_list())['items'][0].blobs_remaining)\n\n    async def test_file_save_streaming_only_save_blobs(self):\n        await self.test_streaming_only_with_blobs()\n        stream = (await self.daemon.jsonrpc_file_list())['items'][0]\n        self.assertIsNone(stream.full_path)\n        self.server.stop_server()\n        await self.daemon.jsonrpc_file_save('test', self.daemon.conf.data_dir)\n        stream = (await self.daemon.jsonrpc_file_list())['items'][0]\n        self.assertIsNotNone(stream.full_path)\n        await stream.finished_writing.wait()\n        with open(stream.full_path, 'rb') as f:\n            self.assertEqual(self.data, f.read())\n        await self.daemon.jsonrpc_file_delete(delete_from_download_dir=True, sd_hash=stream.sd_hash)\n\n    async def test_file_save_stop_before_finished_streaming_only(self, wait_for_start_writing=False):\n        await self.test_streaming_only_with_blobs()\n        stream = (await self.daemon.jsonrpc_file_list())['items'][0]\n        self.assertIsNone(stream.full_path)\n        self.server.stop_server()\n        await self.daemon.jsonrpc_file_save('test', self.daemon.conf.data_dir)\n        stream = (await self.daemon.jsonrpc_file_list())['items'][0]\n        path = stream.full_path\n        self.assertIsNotNone(path)\n        if wait_for_start_writing:\n            with contextlib.suppress(asyncio.CancelledError):\n                await stream.started_writing.wait()\n            self.assertTrue(os.path.isfile(path))\n        await self.daemon.file_manager.stop()\n        # while stopped, we get no response to query and no file is present\n        self.assertEqual((await self.daemon.jsonrpc_file_list())['items'], [])\n        self.assertEqual(os.path.isfile(path), stream.status == ManagedDownloadSource.STATUS_FINISHED)\n        await self.daemon.file_manager.start()\n        # after restart, we get a response to query and same file path\n        stream = (await self.daemon.jsonrpc_file_list())['items'][0]\n        self.assertIsNotNone(stream.full_path)\n        self.assertEqual(stream.full_path, path)\n        if wait_for_start_writing:\n            with contextlib.suppress(asyncio.CancelledError):\n                await stream.started_writing.wait()\n            self.assertTrue(os.path.isfile(path))\n\n    async def test_file_save_stop_before_finished_streaming_only_wait_for_start(self):\n        return await self.test_file_save_stop_before_finished_streaming_only(wait_for_start_writing=True)\n\n    async def test_file_save_streaming_only_dont_save_blobs(self):\n        await self.test_streaming_only_without_blobs()\n        stream = (await self.daemon.jsonrpc_file_list())['items'][0]\n        self.assertIsNone(stream.full_path)\n        await self.daemon.jsonrpc_file_save('test', self.daemon.conf.data_dir)\n        stream = (await self.daemon.jsonrpc_file_list())['items'][0]\n        await stream.finished_writing.wait()\n        with open(stream.full_path, 'rb') as f:\n            self.assertEqual(self.data, f.read())\n\n\nclass RangeRequestsLRUCache(CommandTestCase):\n    blob_lru_cache_size = 32\n\n    async def _request_stream(self):\n        name = 'foo'\n        url = f'http://{self.daemon.conf.streaming_host}:{self.daemon.conf.streaming_port}/get/{name}'\n\n        async with aiohttp_request('get', url) as req:\n            self.assertEqual(req.headers.get('Content-Type'), 'application/octet-stream')\n            content_range = req.headers.get('Content-Range')\n            content_length = int(req.headers.get('Content-Length'))\n            streamed_bytes = await req.content.read()\n        self.assertEqual(content_length, len(streamed_bytes))\n        self.assertEqual(15, content_length)\n        self.assertEqual(b'hi\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00', streamed_bytes)\n        self.assertEqual('bytes 0-14/15', content_range)\n\n    async def test_range_requests_with_blob_lru_cache(self):\n        self.data = b'hi'\n        self.daemon.conf.save_blobs = False\n        self.daemon.conf.save_files = False\n        await self.stream_create('foo', '0.01', data=self.data, file_size=0)\n        await (await self.daemon.jsonrpc_file_list())['items'][0].fully_reflected.wait()\n        await self.daemon.jsonrpc_file_delete(delete_from_download_dir=True, claim_name='foo')\n        self.assertEqual(0, len(os.listdir(self.daemon.blob_manager.blob_dir)))\n\n        await self.daemon.streaming_runner.setup()\n        site = aiohttp.web.TCPSite(self.daemon.streaming_runner, self.daemon.conf.streaming_host,\n                                   self.daemon.conf.streaming_port)\n        await site.start()\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 0)\n\n        await self._request_stream()\n        self.assertItemCount(await self.daemon.jsonrpc_file_list(), 1)\n        self.server.stop_server()\n\n        # running with cache size 0 gets through without errors without\n        # this since the server doesn't stop immediately\n        await asyncio.sleep(1)\n\n        await self._request_stream()\n"
  },
  {
    "path": "tests/integration/other/__init__.py",
    "content": ""
  },
  {
    "path": "tests/integration/other/test_chris45.py",
    "content": "from lbry.testcase import CommandTestCase\n\n\nclass EpicAdventuresOfChris45(CommandTestCase):\n\n    async def test_no_this_is_not_a_test_its_an_adventure(self):\n\n        # Chris45 is an avid user of LBRY and this is his story. It's fact and fiction\n        # and everything in between; it's also the setting of some record setting\n        # integration tests.\n\n        # Chris45 starts everyday by checking his balance.\n        result = await self.daemon.jsonrpc_account_balance()\n        self.assertEqual(result['available'], '10.0')\n        # \"10 LBC, yippy! I can do a lot with that.\", he thinks to himself,\n        # enthusiastically. But he is hungry so he goes into the kitchen\n        # to make himself a spamdwich.\n\n        # While making the spamdwich he wonders... has anyone on LBRY\n        # registered the @spam channel yet? \"I should do that!\" he\n        # exclaims and goes back to his computer to do just that!\n        tx = await self.channel_create('@spam', '1.0')\n        channel_id = self.get_claim_id(tx)\n\n        # Do we have it locally?\n        channels = await self.out(self.daemon.jsonrpc_channel_list())\n        self.assertItemCount(channels, 1)\n        self.assertEqual(channels['items'][0]['name'], '@spam')\n\n        # As the new channel claim travels through the intertubes and makes its\n        # way into the mempool and then a block and then into the claimtrie,\n        # Chris doesn't sit idly by: he checks his balance!\n\n        result = await self.daemon.jsonrpc_account_balance()\n        self.assertEqual(result['available'], '8.989893')\n\n        # He waits for 6 more blocks (confirmations) to make sure the balance has been settled.\n        await self.generate(6)\n        result = await self.daemon.jsonrpc_account_balance(confirmations=6)\n        self.assertEqual(result['available'], '8.989893')\n\n        # And is the channel resolvable and empty?\n        response = await self.resolve('lbry://@spam')\n        self.assertEqual(response['value_type'], 'channel')\n\n        # \"What goes well with spam?\" ponders Chris...\n        # \"A hovercraft with eels!\" he exclaims.\n        # \"That's what goes great with spam!\" he further confirms.\n\n        # And so, many hours later, Chris is finished writing his epic story\n        # about eels driving a hovercraft across the wetlands while eating spam\n        # and decides it's time to publish it to the @spam channel.\n        tx = await self.stream_create(\n            'hovercraft', '1.0',\n            data=b'[insert long story about eels driving hovercraft]',\n            channel_id=channel_id\n        )\n        claim_id = self.get_claim_id(tx)\n\n        # He quickly checks the unconfirmed balance to make sure everything looks\n        # correct.\n        result = await self.daemon.jsonrpc_account_balance()\n        self.assertEqual(result['available'], '7.969786')\n\n        # Also checks that his new story can be found on the blockchain before\n        # giving the link to all his friends.\n        response = await self.resolve('lbry://@spam/hovercraft')\n        self.assertEqual(response['value_type'], 'stream')\n\n        # He goes to tell everyone about it and in the meantime 5 blocks are confirmed.\n        await self.generate(5)\n        # When he comes back he verifies the confirmed balance.\n        result = await self.daemon.jsonrpc_account_balance()\n        self.assertEqual(result['available'], '7.969786')\n\n        # As people start reading his story they discover some typos and notify\n        # Chris who explains in despair \"Oh! Noooooos!\" but then remembers\n        # \"No big deal! I can update my claim.\" And so he updates his claim.\n        await self.stream_update(claim_id, data=b'[typo fixing sounds being made]')\n\n        # After some soul searching Chris decides that his story needs more\n        # heart and a better ending. He takes down the story and begins the rewrite.\n        abandon = await self.out(self.daemon.jsonrpc_stream_abandon(claim_id, blocking=True))\n        self.assertEqual(abandon['inputs'][0]['claim_id'], claim_id)\n        await self.confirm_tx(abandon['txid'])\n\n        # And now checks that the claim doesn't resolve anymore.\n        self.assertEqual(\n            {'error': {\n                'name': 'NOT_FOUND',\n                'text': 'Could not find claim at \"lbry://@spam/hovercraft\".'\n            }},\n            await self.resolve('lbry://@spam/hovercraft')\n        )\n\n        # After abandoning he just waits for his LBCs to be returned to his account\n        await self.generate(5)\n        result = await self.daemon.jsonrpc_account_balance()\n        self.assertEqual(result['available'], '8.9693455')\n\n        # Amidst all this Chris receives a call from his friend Ramsey\n        # who says that it is of utmost urgency that Chris transfer him\n        # 1 LBC to which Chris readily obliges\n        ramsey_account_id = (await self.out(self.daemon.jsonrpc_account_create(\"Ramsey\")))['id']\n        ramsey_address = await self.daemon.jsonrpc_address_unused(ramsey_account_id)\n        result = await self.out(self.daemon.jsonrpc_account_send('1.0', ramsey_address, blocking=True))\n        self.assertIn(\"txid\", result)\n        await self.confirm_tx(result['txid'])\n\n        # Chris then eagerly waits for 6 confirmations to check his balance and then calls Ramsey to verify whether\n        # he received it or not\n        await self.generate(5)\n        result = await self.daemon.jsonrpc_account_balance()\n        # Chris' balance was correct\n        self.assertEqual(result['available'], '7.9692215')\n\n        # Ramsey too assured him that he had received the 1 LBC and thanks him\n        result = await self.daemon.jsonrpc_account_balance(ramsey_account_id)\n        self.assertEqual(result['available'], '1.0')\n\n        # After Chris is done with all the \"helping other people\" stuff he decides that it's time to\n        # write a new story and publish it to lbry. All he needed was a fresh start and he came up with:\n        tx = await self.stream_create(\n            'fresh-start', '1.0', data=b'Amazingly Original First Line', channel_id=channel_id\n        )\n        claim_id2 = self.get_claim_id(tx)\n\n        await self.generate(5)\n\n        # He gives the link of his story to all his friends and hopes that this is the much needed break for him\n        uri = 'lbry://@spam/fresh-start'\n\n        # And voila, and bravo and encore! His Best Friend Ramsey read the story and immediately knew this was a hit\n        # Now to keep this claim winning on the lbry blockchain he immediately supports the claim\n        tx = await self.out(self.daemon.jsonrpc_support_create(\n            claim_id2, '0.2', account_id=ramsey_account_id, blocking=True\n        ))\n        await self.confirm_tx(tx['txid'])\n\n        # And check if his support showed up\n        resolve_result = await self.resolve(uri)\n        # It obviously did! Because, blockchain baby \\O/\n        self.assertEqual(resolve_result['amount'], '1.0')\n        self.assertEqual(resolve_result['meta']['effective_amount'], '1.2')\n        await self.generate(5)\n\n        # Now he also wanted to support the original creator of the Award Winning Novel\n        # So he quickly decides to send a tip to him\n        tx = await self.out(\n            self.daemon.jsonrpc_support_create(claim_id2, '0.3', tip=True, account_id=ramsey_account_id, blocking=True)\n        )\n        await self.confirm_tx(tx['txid'])\n\n        # And again checks if it went to the just right place\n        resolve_result = await self.resolve(uri)\n        # Which it obviously did. Because....?????\n        self.assertEqual(resolve_result['meta']['effective_amount'], '1.5')\n        await self.generate(5)\n\n        # Seeing the ravishing success of his novel Chris adds support to his claim too\n        tx = await self.out(self.daemon.jsonrpc_support_create(claim_id2, '0.4', blocking=True))\n        await self.confirm_tx(tx['txid'])\n\n        # And check if his support showed up\n        resolve_result = await self.out(self.daemon.jsonrpc_resolve(uri))\n        # It did!\n        self.assertEqual(resolve_result[uri]['meta']['effective_amount'], '1.9')\n        await self.generate(5)\n\n        # Now Ramsey who is a singer by profession, is preparing for his new \"gig\". He has everything in place for that\n        # the instruments, the theatre, the ads, everything, EXCEPT lyrics!! He panicked.. But then he remembered\n        # something, so he un-panicked. He quickly calls up his best bud Chris and requests him to write hit lyrics for\n        # his song, seeing as his novel had smashed all the records, he was the perfect candidate!\n        # .......\n        # Chris agrees.. 17 hours 43 minutes and 14 seconds later, he makes his publish\n        tx = await self.stream_create(\n            'hit-song', '1.0', data=b'The Whale and The Bookmark', channel_id=channel_id\n        )\n        await self.generate(5)\n\n        # He sends the link to Ramsey, all happy and proud\n        uri = 'lbry://@spam/hit-song'\n\n        # But sadly Ramsey wasn't so pleased. It was hard for him to tell Chris...\n        # Chris, though a bit heartbroken, abandoned the claim for now, but instantly started working on new hit lyrics\n        abandon = await self.out(self.daemon.jsonrpc_stream_abandon(txid=tx['txid'], nout=0, blocking=True))\n        self.assertTrue(abandon['inputs'][0]['txid'], tx['txid'])\n        await self.confirm_tx(abandon['txid'])\n\n        # He them checks that the claim doesn't resolve anymore.\n        self.assertEqual(\n            {'error': {\n                'name': 'NOT_FOUND',\n                'text': f'Could not find claim at \"{uri}\".'\n            }},\n            await self.resolve(uri)\n        )\n\n        # He closes and opens the wallet server databases to see how horribly they break\n        db = self.conductor.spv_node.server.db\n        db.close()\n        db.open_db()\n        await db.initialize_caches()\n        # They didn't! (error would be AssertionError: 276 vs 266 (264 counts) on startup)\n"
  },
  {
    "path": "tests/integration/other/test_cli.py",
    "content": "import contextlib\nimport os\nimport tempfile\nfrom io import StringIO\nfrom lbry.testcase import AsyncioTestCase\n\nfrom lbry.conf import Config\nfrom lbry.extras import cli\nfrom lbry.extras.daemon.components import (\n    DATABASE_COMPONENT, DISK_SPACE_COMPONENT, BLOB_COMPONENT, WALLET_COMPONENT, DHT_COMPONENT,\n    HASH_ANNOUNCER_COMPONENT, FILE_MANAGER_COMPONENT, PEER_PROTOCOL_SERVER_COMPONENT,\n    UPNP_COMPONENT, EXCHANGE_RATE_MANAGER_COMPONENT, WALLET_SERVER_PAYMENTS_COMPONENT,\n    LIBTORRENT_COMPONENT, BACKGROUND_DOWNLOADER_COMPONENT, TRACKER_ANNOUNCER_COMPONENT\n)\nfrom lbry.extras.daemon.daemon import Daemon\n\n\nclass CLIIntegrationTest(AsyncioTestCase):\n\n    async def asyncSetUp(self):\n        conf = Config()\n        conf.data_dir = '/tmp'\n        conf.share_usage_data = False\n        conf.api = 'localhost:5299'\n        conf.components_to_skip = (\n            DATABASE_COMPONENT, DISK_SPACE_COMPONENT, BLOB_COMPONENT, WALLET_COMPONENT, DHT_COMPONENT,\n            HASH_ANNOUNCER_COMPONENT, FILE_MANAGER_COMPONENT, PEER_PROTOCOL_SERVER_COMPONENT,\n            UPNP_COMPONENT, EXCHANGE_RATE_MANAGER_COMPONENT, WALLET_SERVER_PAYMENTS_COMPONENT,\n            LIBTORRENT_COMPONENT, BACKGROUND_DOWNLOADER_COMPONENT, TRACKER_ANNOUNCER_COMPONENT\n        )\n        Daemon.component_attributes = {}\n        self.daemon = Daemon(conf)\n        await self.daemon.start()\n        self.addCleanup(self.daemon.stop)\n\n    def test_cli_status_command_with_auth(self):\n        actual_output = StringIO()\n        with contextlib.redirect_stdout(actual_output):\n            cli.main([\"--api\", \"localhost:5299\", \"status\"])\n        actual_output = actual_output.getvalue()\n        self.assertIn(\"is_running\", actual_output)\n\n    def test_when_download_dir_non_writable_on_start_then_daemon_dies_with_helpful_msg(self):\n        with tempfile.TemporaryDirectory() as download_dir:\n            os.chmod(download_dir, mode=0o555)  # makes download dir non-writable, readable and executable\n            with self.assertRaisesRegex(PermissionError, f\"The following directory is not writable: {download_dir}\"):\n                cli.main([\"start\", \"--download-dir\", download_dir])\n"
  },
  {
    "path": "tests/integration/other/test_exchange_rate_manager.py",
    "content": "import asyncio\nfrom decimal import Decimal\nfrom lbry.testcase import AsyncioTestCase\nfrom lbry.extras.daemon.exchange_rate_manager import (\n    ExchangeRate, ExchangeRateManager, FEEDS, MarketFeed\n)\n\n\nclass TestExchangeRateManager(AsyncioTestCase):\n#     async def test_exchange_rate_manager(self):\n#         manager = ExchangeRateManager(FEEDS)\n#         manager.start()\n#         self.addCleanup(manager.stop)\n#         for feed in manager.market_feeds:\n#             self.assertFalse(feed.is_online)\n#             self.assertIsNone(feed.rate)\n#         await manager.wait()\n#         failures = set()\n#         for feed in manager.market_feeds:\n#             if feed.is_online:\n#                 self.assertIsInstance(feed.rate, ExchangeRate)\n#             else:\n#                 failures.add(feed.name)\n#                 self.assertFalse(feed.has_rate)\n#         self.assertLessEqual(len(failures), 1, f\"feed failures: {failures}. Please check exchange rate feeds!\")\n#         lbc = manager.convert_currency('USD', 'LBC', Decimal('1.0'))\n#         self.assertGreaterEqual(lbc, 2.0)\n#         self.assertLessEqual(lbc, 120.0)\n#         lbc = manager.convert_currency('BTC', 'LBC', Decimal('0.01'))\n#         self.assertGreaterEqual(lbc, 1_000)\n#         self.assertLessEqual(lbc, 30_000)\n\n    async def test_it_handles_feed_being_offline(self):\n        class FakeFeed(MarketFeed):\n            name = \"fake\"\n            url = \"http://impossi.bru\"\n        manager = ExchangeRateManager((FakeFeed,))\n        manager.start()\n        self.addCleanup(manager.stop)\n        for feed in manager.market_feeds:\n            self.assertFalse(feed.is_online)\n            self.assertIsNone(feed.rate)\n        await asyncio.wait_for(manager.wait(), 2)\n        for feed in manager.market_feeds:\n            self.assertFalse(feed.is_online)\n            self.assertFalse(feed.has_rate)\n"
  },
  {
    "path": "tests/integration/other/test_other_commands.py",
    "content": "from lbry.testcase import CommandTestCase\n\n\nclass AddressManagement(CommandTestCase):\n\n    async def test_address_list(self):\n        addresses = await self.out(self.daemon.jsonrpc_address_list())\n        self.assertItemCount(addresses, 27)\n\n        single = await self.out(self.daemon.jsonrpc_address_list(addresses['items'][11]['address']))\n        self.assertItemCount(single, 1)\n        self.assertEqual(single['items'][0], addresses['items'][11])\n\n\nclass SettingsManagement(CommandTestCase):\n\n    async def test_settings(self):\n        self.assertEqual(self.daemon.jsonrpc_settings_get()['lbryum_servers'][0], ('localhost', 50002))\n\n        setting = self.daemon.jsonrpc_settings_set('lbryum_servers', ['server:50001'])\n        self.assertEqual(setting['lbryum_servers'][0], ('server', 50001))\n        self.assertEqual(self.daemon.jsonrpc_settings_get()['lbryum_servers'][0], ('server', 50001))\n\n        setting = self.daemon.jsonrpc_settings_clear('lbryum_servers')\n        self.assertEqual(setting['lbryum_servers'][0], ('spv11.lbry.com', 50001))\n        self.assertEqual(self.daemon.jsonrpc_settings_get()['lbryum_servers'][0], ('spv11.lbry.com', 50001))\n\n        # test_privacy_settings (merged for reducing test time, unmerge when its fast)\n        # tests that changing share_usage_data propagates to the relevant properties\n        self.assertFalse(self.daemon.jsonrpc_settings_get()['share_usage_data'])\n        self.daemon.jsonrpc_settings_set('share_usage_data', True)\n        self.assertTrue(self.daemon.jsonrpc_settings_get()['share_usage_data'])\n        self.assertTrue(self.daemon.analytics_manager.enabled)\n        self.daemon.jsonrpc_settings_set('share_usage_data', False)\n\n\nclass TroubleshootingCommands(CommandTestCase):\n    async def test_tracemalloc_commands(self):\n        self.addCleanup(self.daemon.jsonrpc_tracemalloc_disable)\n        self.assertFalse(self.daemon.jsonrpc_tracemalloc_disable())\n        self.assertTrue(self.daemon.jsonrpc_tracemalloc_enable())\n\n        class WeirdObject():\n            pass\n        hold_em = [WeirdObject() for _ in range(500)]\n        top = self.daemon.jsonrpc_tracemalloc_top(1)\n        self.assertEqual(1, len(top))\n        self.assertEqual('hold_em = [WeirdObject() for _ in range(500)]', top[0]['code'])\n        self.assertTrue(top[0]['line'].startswith('other/test_other_commands.py:'))\n        self.assertGreaterEqual(top[0]['count'], 500)\n        self.assertGreater(top[0]['size'], 0)  # just matters that its a positive integer\n"
  },
  {
    "path": "tests/integration/other/test_transcoding.py",
    "content": "import logging\nimport pathlib\nimport time\n\nfrom ..claims.test_claim_commands import ClaimTestCase\nfrom lbry.conf import TranscodeConfig\nfrom lbry.file_analysis import VideoFileAnalyzer\n\nlog = logging.getLogger(__name__)\n\n\nclass MeasureTime:\n    def __init__(self, text):\n        print(text, end=\"...\", flush=True)\n\n    def __enter__(self):\n        self.start = time.perf_counter()\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        end = time.perf_counter()\n        print(f\" done in {end - self.start:.6f}s\", flush=True)\n\n\nclass TranscodeValidation(ClaimTestCase):\n\n    def make_name(self, name, extension=\"\"):\n        path = pathlib.Path(self.video_file_name)\n        return path.parent / f\"{path.stem}_{name}{extension or path.suffix}\"\n\n    async def asyncSetUp(self):\n        await super().asyncSetUp()\n        self.conf = TranscodeConfig()\n        self.conf.volume_analysis_time = 0  # disable it as the test file isn't very good here\n        self.analyzer = VideoFileAnalyzer(self.conf)\n        self.assertTrue((await self.analyzer.status())[\"available\"])  # ensure ffmpeg path detected\n        file_ogg = self.make_name(\"ogg\", \".ogg\")\n        self.video_file_ogg = str(file_ogg)\n        if not file_ogg.exists():\n            command = f'-i \"{self.video_file_name}\" -c:v libtheora -q:v 4 -c:a libvorbis -q:a 4 ' \\\n                      f'-c:s copy -c:d copy \"{file_ogg}\"'\n            with MeasureTime(f\"Creating {file_ogg.name}\"):\n                output, code = await self.analyzer._execute_ffmpeg(command)\n                self.assertEqual(code, 0, output)\n\n        file_webm = self.make_name(\"webm\", \".webm\")\n        self.video_file_webm = str(file_webm)\n        if not file_webm.exists():\n            command = f'-i \"{self.video_file_name}\" -c:v libvpx-vp9 -crf 36 -b:v 0 -cpu-used 2 ' \\\n                      f'-c:a libopus -b:a 128k -c:s copy -c:d copy \"{file_webm}\"'\n            with MeasureTime(f\"Creating {file_webm.name}\"):\n                output, code = await self.analyzer._execute_ffmpeg(command)\n                self.assertEqual(code, 0, output)\n\n    async def test_should_work(self):\n        new_file_name, _ = await self.analyzer.verify_or_repair(True, False, self.video_file_name)\n        self.assertEqual(self.video_file_name, new_file_name)\n        new_file_name, _ = await self.analyzer.verify_or_repair(True, False, self.video_file_ogg)\n        self.assertEqual(self.video_file_ogg, new_file_name)\n        new_file_name, spec = await self.analyzer.verify_or_repair(True, False, self.video_file_webm)\n        self.assertEqual(self.video_file_webm, new_file_name)\n        self.assertEqual(spec[\"width\"], 1280)\n        self.assertEqual(spec[\"height\"], 720)\n        self.assertEqual(spec[\"duration\"], 16)\n\n    async def test_volume(self):\n        self.conf.volume_analysis_time = 200\n        with self.assertRaisesRegex(Exception, \"lower than prime\"):\n            await self.analyzer.verify_or_repair(True, False, self.video_file_name)\n\n    async def test_container(self):\n        file_name = self.make_name(\"bad_container\", \".avi\")\n        if not file_name.exists():\n            command = f'-i \"{self.video_file_name}\" -c copy -map 0 \"{file_name}\"'\n            with MeasureTime(f\"Creating {file_name.name}\"):\n                output, code = await self.analyzer._execute_ffmpeg(command)\n                self.assertEqual(code, 0, output)\n\n        with self.assertRaisesRegex(Exception, \"Container format is not in the approved list\"):\n            await self.analyzer.verify_or_repair(True, False, file_name)\n\n        fixed_file, _ = await self.analyzer.verify_or_repair(True, True, file_name)\n        pathlib.Path(fixed_file).unlink()\n\n    async def test_video_codec(self):\n        file_name = self.make_name(\"bad_video_codec_1\")\n        if not file_name.exists():\n            command = f'-i \"{self.video_file_name}\" -c copy -map 0 -c:v libx265 -preset superfast \"{file_name}\"'\n            with MeasureTime(f\"Creating {file_name.name}\"):\n                output, code = await self.analyzer._execute_ffmpeg(command)\n                self.assertEqual(code, 0, output)\n\n        with self.assertRaisesRegex(Exception, \"Video codec is not in the approved list\"):\n            await self.analyzer.verify_or_repair(True, False, file_name)\n        with self.assertRaisesRegex(Exception, \"faststart flag was not used\"):\n            await self.analyzer.verify_or_repair(True, False, file_name)\n\n        fixed_file, _ = await self.analyzer.verify_or_repair(True, True, file_name)\n        pathlib.Path(fixed_file).unlink()\n\n    async def test_max_bit_rate(self):\n        self.conf.video_bitrate_maximum = 100\n        with self.assertRaisesRegex(Exception, \"The bit rate is above the configured maximum\"):\n            await self.analyzer.verify_or_repair(True, False, self.video_file_name)\n\n    async def test_video_format(self):\n        file_name = self.make_name(\"bad_video_format_1\")\n        if not file_name.exists():\n            command = f'-i \"{self.video_file_name}\" -c copy -map 0 -c:v libx264 ' \\\n                      f'-vf format=yuv444p \"{file_name}\"'\n            with MeasureTime(f\"Creating {file_name.name}\"):\n                output, code = await self.analyzer._execute_ffmpeg(command)\n                self.assertEqual(code, 0, output)\n\n        with self.assertRaisesRegex(Exception, \"pixel format does not match the approved\"):\n            await self.analyzer.verify_or_repair(True, False, file_name)\n\n        fixed_file, _ = await self.analyzer.verify_or_repair(True, True, file_name)\n        pathlib.Path(fixed_file).unlink()\n\n    async def test_audio_codec(self):\n        file_name = self.make_name(\"bad_audio_codec_1\", \".mkv\")\n        if not file_name.exists():\n            command = f'-i \"{self.video_file_name}\" -c copy -map 0 -c:a pcm_s16le \"{file_name}\"'\n            with MeasureTime(f\"Creating {file_name.name}\"):\n                output, code = await self.analyzer._execute_ffmpeg(command)\n                self.assertEqual(code, 0, output)\n\n        with self.assertRaisesRegex(Exception, \"Audio codec is not in the approved list\"):\n            await self.analyzer.verify_or_repair(True, False, file_name)\n\n        fixed_file, _ = await self.analyzer.verify_or_repair(True, True, file_name)\n        pathlib.Path(fixed_file).unlink()\n\n    async def test_extension_choice(self):\n\n        scan_data = await self.analyzer._get_scan_data(True, self.video_file_name)\n        extension = self.analyzer._get_best_container_extension(scan_data, \"\")\n        self.assertEqual(extension, pathlib.Path(self.video_file_name).suffix[1:])\n\n        scan_data = await self.analyzer._get_scan_data(True, self.video_file_ogg)\n        extension = self.analyzer._get_best_container_extension(scan_data, \"\")\n        self.assertEqual(extension, \"ogv\")\n\n        scan_data = await self.analyzer._get_scan_data(True, self.video_file_webm)\n        extension = self.analyzer._get_best_container_extension(scan_data, \"\")\n        self.assertEqual(extension, \"webm\")\n\n        extension = self.analyzer._get_best_container_extension(\"\", \"libx264 -crf 23\")\n        self.assertEqual(\"mp4\", extension)\n\n        extension = self.analyzer._get_best_container_extension(\"\", \"libvpx-vp9 -crf 23\")\n        self.assertEqual(\"webm\", extension)\n\n        extension = self.analyzer._get_best_container_extension(\"\", \"libtheora\")\n        self.assertEqual(\"ogv\", extension)\n\n    async def test_no_ffmpeg(self):\n        self.conf.ffmpeg_path = \"I don't really exist/\"\n        self.analyzer._env_copy.pop(\"PATH\", None)\n        await self.analyzer.status(reset=True)\n        with self.assertRaisesRegex(Exception, \"Unable to locate\"):\n            await self.analyzer.verify_or_repair(True, False, self.video_file_name)\n\n    async def test_dont_recheck_ffmpeg_installation(self):\n\n        call_count = 0\n\n        original = self.daemon._video_file_analyzer._verify_ffmpeg_installed\n\n        def _verify_ffmpeg_installed():\n            nonlocal call_count\n            call_count += 1\n            return original()\n\n        self.daemon._video_file_analyzer._verify_ffmpeg_installed = _verify_ffmpeg_installed\n        self.assertEqual(0, call_count)\n        await self.daemon.jsonrpc_status()\n        self.assertEqual(1, call_count)\n        # counter should not go up again\n        await self.daemon.jsonrpc_status()\n        self.assertEqual(1, call_count)\n\n        # this should force rechecking the installation\n        await self.daemon.jsonrpc_ffmpeg_find()\n        self.assertEqual(2, call_count)\n"
  },
  {
    "path": "tests/integration/takeovers/__init__.py",
    "content": ""
  },
  {
    "path": "tests/integration/takeovers/test_resolve_command.py",
    "content": "import asyncio\nimport json\nimport hashlib\nimport sys\nfrom bisect import bisect_right\nfrom binascii import hexlify, unhexlify\nfrom collections import defaultdict\nfrom typing import NamedTuple, List\nfrom lbry.testcase import CommandTestCase\nfrom lbry.wallet.transaction import Transaction, Output\nfrom lbry.schema.compat import OldClaimMessage\nfrom lbry.crypto.hash import sha256\nfrom lbry.crypto.base58 import Base58\n\n\nclass ClaimStateValue(NamedTuple):\n    claim_id: str\n    activation_height: int\n    active_in_lbrycrd: bool\n\n\nclass BaseResolveTestCase(CommandTestCase):\n\n    def assertMatchESClaim(self, claim_from_es, claim_from_db):\n        self.assertEqual(claim_from_es['claim_hash'][::-1].hex(), claim_from_db.claim_hash.hex())\n        self.assertEqual(claim_from_es['claim_id'], claim_from_db.claim_hash.hex())\n        self.assertEqual(claim_from_es['activation_height'], claim_from_db.activation_height, f\"es height: {claim_from_es['activation_height']}, rocksdb height: {claim_from_db.activation_height}\")\n        self.assertEqual(claim_from_es['last_take_over_height'], claim_from_db.last_takeover_height)\n        self.assertEqual(claim_from_es['tx_id'], claim_from_db.tx_hash[::-1].hex())\n        self.assertEqual(claim_from_es['tx_nout'], claim_from_db.position)\n        self.assertEqual(claim_from_es['amount'], claim_from_db.amount)\n        self.assertEqual(claim_from_es['effective_amount'], claim_from_db.effective_amount)\n\n    def assertMatchDBClaim(self, expected, claim):\n        self.assertEqual(expected['claimid'], claim.claim_hash.hex())\n        self.assertEqual(expected['validatheight'], claim.activation_height)\n        self.assertEqual(expected['lasttakeoverheight'], claim.last_takeover_height)\n        self.assertEqual(expected['txid'], claim.tx_hash[::-1].hex())\n        self.assertEqual(expected['n'], claim.position)\n        self.assertEqual(expected['amount'], claim.amount)\n        self.assertEqual(expected['effectiveamount'], claim.effective_amount)\n\n    async def assertResolvesToClaimId(self, name, claim_id):\n        other = await self.resolve(name)\n        if claim_id is None:\n            self.assertIn('error', other)\n            self.assertEqual(other['error']['name'], 'NOT_FOUND')\n            claims_from_es = (await self.conductor.spv_node.server.session_manager.search_index.search(name=name))[0]\n            claims_from_es = [c['claim_hash'][::-1].hex() for c in claims_from_es]\n            self.assertNotIn(claim_id, claims_from_es)\n        else:\n            claim_from_es = await self.conductor.spv_node.server.session_manager.search_index.search(claim_id=claim_id)\n            self.assertEqual(claim_id, other['claim_id'])\n            self.assertEqual(claim_id, claim_from_es[0][0]['claim_hash'][::-1].hex())\n\n    async def assertNoClaimForName(self, name: str):\n        lbrycrd_winning = json.loads(await self.blockchain._cli_cmnd('getclaimsforname', name))\n        stream, channel, _, _ = await self.conductor.spv_node.server.db.resolve(name)\n        if 'claims' in lbrycrd_winning and lbrycrd_winning['claims'] is not None:\n            self.assertEqual(len(lbrycrd_winning['claims']), 0)\n        if stream is not None:\n            self.assertIsInstance(stream, LookupError)\n        else:\n            self.assertIsInstance(channel, LookupError)\n        claim_from_es = await self.conductor.spv_node.server.session_manager.search_index.search(name=name)\n        self.assertListEqual([], claim_from_es[0])\n\n    async def assertNoClaim(self, name: str, claim_id: str):\n        expected = json.loads(await self.blockchain._cli_cmnd('getclaimsfornamebyid', name, '[\"' + claim_id + '\"]'))\n        if 'claims' in expected and expected['claims'] is not None:\n            # ensure that if we do have the matching claim that it is not active\n            self.assertEqual(expected['claims'][0]['effectiveamount'], 0)\n\n        claim_from_es = await self.conductor.spv_node.server.session_manager.search_index.search(claim_id=claim_id)\n        self.assertListEqual([], claim_from_es[0])\n        claim = await self.conductor.spv_node.server.db.fs_getclaimbyid(claim_id)\n        self.assertIsNone(claim)\n\n    async def assertMatchWinningClaim(self, name):\n        expected = json.loads(await self.blockchain._cli_cmnd('getclaimsfornamebybid', name, \"[0]\"))\n        stream, channel, _, _ = await self.conductor.spv_node.server.db.resolve(name)\n        claim = stream if stream else channel\n        expected['claims'][0]['lasttakeoverheight'] = expected['lasttakeoverheight']\n        await self._assertMatchClaim(expected['claims'][0], claim)\n        return claim\n\n    async def _assertMatchClaim(self, expected, claim):\n        self.assertMatchDBClaim(expected, claim)\n        claim_from_es = await self.conductor.spv_node.server.session_manager.search_index.search(\n            claim_id=claim.claim_hash.hex()\n        )\n        self.assertEqual(len(claim_from_es[0]), 1)\n        self.assertMatchESClaim(claim_from_es[0][0], claim)\n        self._check_supports(claim.claim_hash.hex(), expected.get('supports', []),\n                             claim_from_es[0][0]['support_amount'])\n\n    async def assertMatchClaim(self, name, claim_id, is_active_in_lbrycrd=True):\n        claim = await self.conductor.spv_node.server.db.fs_getclaimbyid(claim_id)\n        claim_from_es = await self.conductor.spv_node.server.session_manager.search_index.search(\n            claim_id=claim.claim_hash.hex()\n        )\n        self.assertEqual(len(claim_from_es[0]), 1)\n        self.assertEqual(claim_from_es[0][0]['claim_hash'][::-1].hex(), claim.claim_hash.hex())\n        self.assertMatchESClaim(claim_from_es[0][0], claim)\n\n        expected = json.loads(await self.blockchain._cli_cmnd('getclaimsfornamebyid', name, '[\"' + claim_id + '\"]'))\n        if is_active_in_lbrycrd:\n            if not expected:\n                self.assertIsNone(claim)\n                return\n            expected['claims'][0]['lasttakeoverheight'] = expected['lasttakeoverheight']\n            self.assertMatchDBClaim(expected['claims'][0], claim)\n            self._check_supports(claim.claim_hash.hex(), expected['claims'][0].get('supports', []),\n                                 claim_from_es[0][0]['support_amount'])\n        else:\n            if 'claims' in expected and expected['claims'] is not None:\n                # ensure that if we do have the matching claim that it is not active\n                self.assertEqual(expected['claims'][0]['effectiveamount'], 0)\n        return claim\n\n    async def assertMatchClaimIsWinning(self, name, claim_id):\n        self.assertEqual(claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex())\n        await self.assertMatchClaimsForName(name)\n\n    def _check_supports(self, claim_id, lbrycrd_supports, es_support_amount):\n        total_lbrycrd_amount = 0.0\n        total_es_amount = 0.0\n        active_es_amount = 0.0\n        db = self.conductor.spv_node.server.db\n        es_supports = db.get_supports(bytes.fromhex(claim_id))\n\n        # we're only concerned about active supports here, and they should match\n        self.assertTrue(len(es_supports) >= len(lbrycrd_supports))\n\n        for i, (tx_num, position, amount) in enumerate(es_supports):\n            total_es_amount += amount\n            valid_height = db.get_activation(tx_num, position, is_support=True)\n            if valid_height > db.db_height:\n                continue\n            active_es_amount += amount\n            txid = db.prefix_db.tx_hash.get(tx_num, deserialize_value=False)[::-1].hex()\n            support = next(filter(lambda s: s['txid'] == txid and s['n'] == position, lbrycrd_supports))\n            total_lbrycrd_amount += support['amount']\n            self.assertEqual(support['height'], bisect_right(db.tx_counts, tx_num))\n            self.assertEqual(support['validatheight'], valid_height)\n\n        self.assertEqual(total_es_amount, es_support_amount)\n        self.assertEqual(active_es_amount, total_lbrycrd_amount)\n\n    async def assertMatchClaimsForName(self, name):\n        expected = json.loads(await self.blockchain._cli_cmnd('getclaimsforname', name, \"\", \"true\"))\n        db = self.conductor.spv_node.server.db\n\n        for c in expected['claims']:\n            c['lasttakeoverheight'] = expected['lasttakeoverheight']\n            claim_id = c['claimid']\n            claim_hash = bytes.fromhex(claim_id)\n            claim = db._fs_get_claim_by_hash(claim_hash)\n            self.assertMatchDBClaim(c, claim)\n\n            claim_from_es = await self.conductor.spv_node.server.session_manager.search_index.search(\n                claim_id=claim_id\n            )\n            self.assertEqual(len(claim_from_es[0]), 1)\n            self.assertEqual(claim_from_es[0][0]['claim_hash'][::-1].hex(), claim_id)\n            self.assertMatchESClaim(claim_from_es[0][0], claim)\n            self._check_supports(claim_id, c.get('supports', []),\n                                 claim_from_es[0][0]['support_amount'])\n\n    async def assertNameState(self, height: int, name: str, winning_claim_id: str, last_takeover_height: int,\n                               non_winning_claims: List[ClaimStateValue]):\n        self.assertEqual(height, self.conductor.spv_node.server.db.db_height)\n        await self.assertMatchClaimIsWinning(name, winning_claim_id)\n        for non_winning in non_winning_claims:\n            claim = await self.assertMatchClaim(\n                name, non_winning.claim_id, is_active_in_lbrycrd=non_winning.active_in_lbrycrd\n            )\n            self.assertEqual(non_winning.activation_height, claim.activation_height)\n            self.assertEqual(last_takeover_height, claim.last_takeover_height)\n\n\nclass ResolveCommand(BaseResolveTestCase):\n    async def test_colliding_short_id(self):\n        prefixes = defaultdict(list)\n\n        colliding_claim_ids = []\n        first_claims_one_char_shortid = {}\n\n        while True:\n            chan = self.get_claim_id(\n                await self.channel_create('@abc', '0.01', allow_duplicate_name=True)\n            )\n            if chan[:1] not in first_claims_one_char_shortid:\n                first_claims_one_char_shortid[chan[:1]] = chan\n            prefixes[chan[:2]].append(chan)\n            if len(prefixes[chan[:2]]) > 1:\n                colliding_claim_ids.extend(prefixes[chan[:2]])\n                break\n        first_claim = first_claims_one_char_shortid[colliding_claim_ids[0][:1]]\n        await self.assertResolvesToClaimId(\n            f'@abc#{colliding_claim_ids[0][:1]}', first_claim\n        )\n        collision_depth = 0\n        for c1, c2 in zip(colliding_claim_ids[0], colliding_claim_ids[1]):\n            if c1 == c2:\n                collision_depth += 1\n            else:\n                break\n        await self.assertResolvesToClaimId(f'@abc#{colliding_claim_ids[0][:2]}', colliding_claim_ids[0])\n        await self.assertResolvesToClaimId(f'@abc#{colliding_claim_ids[0][:7]}', colliding_claim_ids[0])\n        await self.assertResolvesToClaimId(f'@abc#{colliding_claim_ids[0][:17]}', colliding_claim_ids[0])\n        await self.assertResolvesToClaimId(f'@abc#{colliding_claim_ids[0]}', colliding_claim_ids[0])\n        await self.assertResolvesToClaimId(f'@abc#{colliding_claim_ids[1][:collision_depth + 1]}', colliding_claim_ids[1])\n        await self.assertResolvesToClaimId(f'@abc#{colliding_claim_ids[1][:7]}', colliding_claim_ids[1])\n        await self.assertResolvesToClaimId(f'@abc#{colliding_claim_ids[1][:17]}', colliding_claim_ids[1])\n        await self.assertResolvesToClaimId(f'@abc#{colliding_claim_ids[1]}', colliding_claim_ids[1])\n\n        # test resolving different streams for a channel using short urls\n        self.get_claim_id(\n            await self.stream_create('foo1', '0.01', channel_id=colliding_claim_ids[0])\n        )\n        self.get_claim_id(\n            await self.stream_create('foo2', '0.01', channel_id=colliding_claim_ids[0])\n        )\n        duplicated_resolved = list((\n            await self.ledger.resolve([], [\n                f'@abc#{colliding_claim_ids[0][:2]}/foo1', f'@abc#{colliding_claim_ids[0][:2]}/foo2'\n            ])\n        ).values())\n        self.assertEqual('foo1', duplicated_resolved[0].normalized_name)\n        self.assertEqual('foo2', duplicated_resolved[1].normalized_name)\n\n    async def test_abandon_channel_and_claims_in_same_tx(self):\n        channel_id = self.get_claim_id(\n            await self.channel_create('@abc', '0.01')\n        )\n        await self.stream_create('foo', '0.01', channel_id=channel_id)\n        await self.channel_update(channel_id, bid='0.001')\n        foo2_id = self.get_claim_id(await self.stream_create('foo2', '0.01', channel_id=channel_id))\n        await self.stream_update(foo2_id, bid='0.0001', channel_id=channel_id, confirm=False)\n        tx = await self.stream_create('foo3', '0.01', channel_id=channel_id, confirm=False, return_tx=True)\n        await self.ledger.wait(tx)\n\n        # db = self.conductor.spv_node.server.bp.db\n        # claims = list(db.all_claims_producer())\n        # print(\"claims\", claims)\n        await self.daemon.jsonrpc_txo_spend(blocking=True)\n        await self.generate(1)\n        await self.assertNoClaimForName('@abc')\n        await self.assertNoClaimForName('foo')\n        await self.assertNoClaimForName('foo2')\n        await self.assertNoClaimForName('foo3')\n\n    async def test_resolve_response(self):\n        channel_id = self.get_claim_id(\n            await self.channel_create('@abc', '0.01')\n        )\n\n        # resolving a channel @abc\n        response = await self.resolve('lbry://@abc')\n        self.assertEqual(response['name'], '@abc')\n        self.assertEqual(response['value_type'], 'channel')\n        self.assertEqual(response['meta']['claims_in_channel'], 0)\n\n        await self.stream_create('foo', '0.01', channel_id=channel_id)\n        await self.stream_create('foo2', '0.01', channel_id=channel_id)\n\n        # resolving a channel @abc with some claims in it\n        response['confirmations'] += 2\n        response['meta']['claims_in_channel'] = 2\n        self.assertEqual(response, await self.resolve('lbry://@abc'))\n\n        # resolving claim foo within channel @abc\n        claim = await self.resolve('lbry://@abc/foo')\n        self.assertEqual(claim['name'], 'foo')\n        self.assertEqual(claim['value_type'], 'stream')\n        self.assertEqual(claim['signing_channel']['name'], '@abc')\n        self.assertTrue(claim['is_channel_signature_valid'])\n        self.assertEqual(\n            claim['timestamp'],\n            self.ledger.headers.estimated_timestamp(claim['height'])\n        )\n        self.assertEqual(\n            claim['signing_channel']['timestamp'],\n            self.ledger.headers.estimated_timestamp(claim['signing_channel']['height'])\n        )\n\n        # resolving claim foo by itself\n        self.assertEqual(claim, await self.resolve('lbry://foo'))\n        # resolving from the given permanent url\n        self.assertEqual(claim, await self.resolve(claim['permanent_url']))\n\n        # resolving multiple at once\n        response = await self.out(self.daemon.jsonrpc_resolve(['lbry://foo', 'lbry://foo2']))\n        self.assertSetEqual({'lbry://foo', 'lbry://foo2'}, set(response))\n        claim = response['lbry://foo2']\n        self.assertEqual(claim['name'], 'foo2')\n        self.assertEqual(claim['value_type'], 'stream')\n        self.assertEqual(claim['signing_channel']['name'], '@abc')\n        self.assertTrue(claim['is_channel_signature_valid'])\n\n        # resolve has correct confirmations\n        tx_details = await self.blockchain.get_raw_transaction(claim['txid'])\n        self.assertEqual(claim['confirmations'], json.loads(tx_details)['confirmations'])\n\n        # FIXME :  claimname/updateclaim is gone. #3480 wip, unblock #3479\"\n        # resolve handles invalid data\n        # await self.blockchain_claim_name(\"gibberish\", hexlify(b\"{'invalid':'json'}\").decode(), \"0.1\")\n        # await self.generate(1)\n        # response = await self.out(self.daemon.jsonrpc_resolve(\"lbry://gibberish\"))\n        # self.assertSetEqual({'lbry://gibberish'}, set(response))\n        # claim = response['lbry://gibberish']\n        # self.assertEqual(claim['name'], 'gibberish')\n        # self.assertNotIn('value', claim)\n\n        # resolve retries\n        await self.conductor.spv_node.stop()\n        resolve_task = asyncio.create_task(self.resolve('foo'))\n        await self.conductor.spv_node.start(self.conductor.lbcwallet_node)\n        self.assertIsNotNone((await resolve_task)['claim_id'])\n\n    async def test_winning_by_effective_amount(self):\n        # first one remains winner unless something else changes\n        claim_id1 = self.get_claim_id(\n            await self.channel_create('@foo', allow_duplicate_name=True))\n        await self.assertResolvesToClaimId('@foo', claim_id1)\n        claim_id2 = self.get_claim_id(\n            await self.channel_create('@foo', allow_duplicate_name=True))\n        await self.assertResolvesToClaimId('@foo', claim_id1)\n        claim_id3 = self.get_claim_id(\n            await self.channel_create('@foo', allow_duplicate_name=True))\n        await self.assertResolvesToClaimId('@foo', claim_id1)\n        # supports change the winner\n        await self.support_create(claim_id3, '0.09')\n        await self.assertResolvesToClaimId('@foo', claim_id3)\n        await self.support_create(claim_id2, '0.19')\n        await self.assertResolvesToClaimId('@foo', claim_id2)\n        await self.support_create(claim_id1, '0.29')\n        await self.assertResolvesToClaimId('@foo', claim_id1)\n\n        await self.support_abandon(claim_id1)\n        await self.assertResolvesToClaimId('@foo', claim_id2)\n\n    async def test_resolve_duplicate_name_in_channel(self):\n        db_resolve = self.conductor.spv_node.server.db.resolve\n        # first one remains winner unless something else changes\n        channel_id = self.get_claim_id(await self.channel_create('@foo'))\n\n        file_path = self.create_upload_file(data=b'hi!')\n        tx = await self.daemon.jsonrpc_stream_create('duplicate', '0.1', file_path=file_path, allow_duplicate_name=True, channel_id=channel_id)\n        await self.ledger.wait(tx)\n\n        first_claim = tx.outputs[0].claim_id\n\n        file_path = self.create_upload_file(data=b'hi!')\n        tx = await self.daemon.jsonrpc_stream_create('duplicate', '0.1', file_path=file_path, allow_duplicate_name=True, channel_id=channel_id)\n        await self.ledger.wait(tx)\n        duplicate_claim = tx.outputs[0].claim_id\n        await self.generate(1)\n\n        stream, channel, _, _ = await db_resolve(f\"@foo:{channel_id}/duplicate:{first_claim}\")\n        self.assertEqual(stream.claim_hash.hex(), first_claim)\n        self.assertEqual(channel.claim_hash.hex(), channel_id)\n        stream, channel, _, _ = await db_resolve(f\"@foo:{channel_id}/duplicate:{duplicate_claim}\")\n        self.assertEqual(stream.claim_hash.hex(), duplicate_claim)\n        self.assertEqual(channel.claim_hash.hex(), channel_id)\n\n    async def test_advanced_resolve(self):\n        claim_id1 = self.get_claim_id(\n            await self.stream_create('foo', '0.7', allow_duplicate_name=True))\n        await self.assertResolvesToClaimId('foo$1', claim_id1)\n        claim_id2 = self.get_claim_id(\n            await self.stream_create('foo', '0.8', allow_duplicate_name=True))\n        await self.assertResolvesToClaimId('foo$1', claim_id2)\n        await self.assertResolvesToClaimId('foo$2', claim_id1)\n        claim_id3 = self.get_claim_id(\n            await self.stream_create('foo', '0.9', allow_duplicate_name=True))\n        # plain winning claim\n        await self.assertResolvesToClaimId('foo', claim_id3)\n\n        # amount order resolution\n        await self.assertResolvesToClaimId('foo$1', claim_id3)\n        await self.assertResolvesToClaimId('foo$2', claim_id2)\n        await self.assertResolvesToClaimId('foo$3', claim_id1)\n        await self.assertResolvesToClaimId('foo$4', None)\n\n    # async def test_partial_claim_id_resolve(self):\n    #     # add some noise\n    #     await self.channel_create('@abc', '0.1', allow_duplicate_name=True)\n    #     await self.channel_create('@abc', '0.2', allow_duplicate_name=True)\n    #     await self.channel_create('@abc', '1.0', allow_duplicate_name=True)\n    #\n    #     channel_id = self.get_claim_id(await self.channel_create('@abc', '1.1', allow_duplicate_name=True))\n    #     await self.assertResolvesToClaimId(f'@abc', channel_id)\n    #     await self.assertResolvesToClaimId(f'@abc#{channel_id[:10]}', channel_id)\n    #     await self.assertResolvesToClaimId(f'@abc#{channel_id}', channel_id)\n    #\n    #     channel = await self.claim_get(channel_id)\n    #     await self.assertResolvesToClaimId(channel['short_url'], channel_id)\n    #     await self.assertResolvesToClaimId(channel['canonical_url'], channel_id)\n    #     await self.assertResolvesToClaimId(channel['permanent_url'], channel_id)\n    #\n    #     # add some noise\n    #     await self.stream_create('foo', '0.1', allow_duplicate_name=True, channel_id=channel['claim_id'])\n    #     await self.stream_create('foo', '0.2', allow_duplicate_name=True, channel_id=channel['claim_id'])\n    #     await self.stream_create('foo', '0.3', allow_duplicate_name=True, channel_id=channel['claim_id'])\n    #\n    #     claim_id1 = self.get_claim_id(\n    #         await self.stream_create('foo', '0.7', allow_duplicate_name=True, channel_id=channel['claim_id']))\n    #     claim1 = await self.claim_get(claim_id=claim_id1)\n    #\n    #     await self.assertResolvesToClaimId('foo', claim_id1)\n    #     await self.assertResolvesToClaimId('@abc/foo', claim_id1)\n    #     await self.assertResolvesToClaimId(claim1['short_url'], claim_id1)\n    #     await self.assertResolvesToClaimId(claim1['canonical_url'], claim_id1)\n    #     await self.assertResolvesToClaimId(claim1['permanent_url'], claim_id1)\n    #\n    #     claim_id2 = self.get_claim_id(\n    #         await self.stream_create('foo', '0.8', allow_duplicate_name=True, channel_id=channel['claim_id']))\n    #     claim2 = await self.claim_get(claim_id=claim_id2)\n    #     await self.assertResolvesToClaimId('foo', claim_id2)\n    #     await self.assertResolvesToClaimId('@abc/foo', claim_id2)\n    #     await self.assertResolvesToClaimId(claim2['short_url'], claim_id2)\n    #     await self.assertResolvesToClaimId(claim2['canonical_url'], claim_id2)\n    #     await self.assertResolvesToClaimId(claim2['permanent_url'], claim_id2)\n\n    async def test_abandoned_channel_with_signed_claims(self):\n        channel = (await self.channel_create('@abc', '1.0'))['outputs'][0]\n        orphan_claim = await self.stream_create('on-channel-claim', '0.0001', channel_id=channel['claim_id'])\n        abandoned_channel_id = channel['claim_id']\n        await self.channel_abandon(txid=channel['txid'], nout=0)\n        channel = (await self.channel_create('@abc', '1.0'))['outputs'][0]\n        orphan_claim_id = self.get_claim_id(orphan_claim)\n\n        # Original channel doesn't exists anymore, so the signature is invalid. For invalid signatures, resolution is\n        # only possible outside a channel\n        self.assertEqual(\n            {'error': {\n                'name': 'NOT_FOUND',\n                'text': 'Could not find claim at \"lbry://@abc/on-channel-claim\".',\n            }},\n            await self.resolve('lbry://@abc/on-channel-claim')\n        )\n        response = await self.resolve('lbry://on-channel-claim')\n        self.assertFalse(response['is_channel_signature_valid'])\n        self.assertEqual({'channel_id': abandoned_channel_id}, response['signing_channel'])\n        direct_uri = 'lbry://on-channel-claim#' + orphan_claim_id\n        response = await self.resolve(direct_uri)\n        self.assertFalse(response['is_channel_signature_valid'])\n        self.assertEqual({'channel_id': abandoned_channel_id}, response['signing_channel'])\n        await self.stream_abandon(claim_id=orphan_claim_id)\n\n        uri = 'lbry://@abc/on-channel-claim'\n        # now, claim something on this channel (it will update the invalid claim, but we save and forcefully restore)\n        valid_claim = await self.stream_create('on-channel-claim', '0.00000001', channel_id=channel['claim_id'])\n        # resolves normally\n        response = await self.resolve(uri)\n        self.assertTrue(response['is_channel_signature_valid'])\n\n        # ooops! claimed a valid conflict! (this happens on the wild, mostly by accident or race condition)\n        await self.stream_create(\n            'on-channel-claim', '0.00000001', channel_id=channel['claim_id'], allow_duplicate_name=True\n        )\n\n        # it still resolves! but to the older claim\n        response = await self.resolve(uri)\n        self.assertTrue(response['is_channel_signature_valid'])\n        self.assertEqual(response['txid'], valid_claim['txid'])\n        claims = [await self.resolve('on-channel-claim'), await self.resolve('on-channel-claim$2')]\n        self.assertEqual(2, len(claims))\n        self.assertEqual(\n            {channel['claim_id']}, {claim['signing_channel']['claim_id'] for claim in claims}\n        )\n\n    async def test_normalization_resolution(self):\n\n        one = 'ΣίσυφοςﬁÆ'\n        two = 'ΣΊΣΥΦΟσFIæ'\n\n        c1 = await self.stream_create(one, '0.1')\n        c2 = await self.stream_create(two, '0.2')\n\n        loser_id = self.get_claim_id(c1)\n        winner_id = self.get_claim_id(c2)\n\n        # winning_one = await self.check_lbrycrd_winning(one)\n        await self.assertMatchClaimIsWinning(two, winner_id)\n\n        claim1 = await self.resolve(f'lbry://{one}')\n        claim2 = await self.resolve(f'lbry://{two}')\n        claim3 = await self.resolve(f'lbry://{one}:{winner_id[:5]}')\n        claim4 = await self.resolve(f'lbry://{two}:{winner_id[:5]}')\n\n        claim5 = await self.resolve(f'lbry://{one}:{loser_id[:5]}')\n        claim6 = await self.resolve(f'lbry://{two}:{loser_id[:5]}')\n\n        self.assertEqual(winner_id, claim1['claim_id'])\n        self.assertEqual(winner_id, claim2['claim_id'])\n        self.assertEqual(winner_id, claim3['claim_id'])\n        self.assertEqual(winner_id, claim4['claim_id'])\n\n        self.assertEqual(two, claim1['name'])\n        self.assertEqual(two, claim2['name'])\n        self.assertEqual(two, claim3['name'])\n        self.assertEqual(two, claim4['name'])\n\n        self.assertEqual(loser_id, claim5['claim_id'])\n        self.assertEqual(loser_id, claim6['claim_id'])\n        self.assertEqual(one, claim5['name'])\n        self.assertEqual(one, claim6['name'])\n\n    async def test_resolve_old_claim(self):\n        channel = await self.daemon.jsonrpc_channel_create('@olds', '1.0', blocking=True)\n        await self.confirm_tx(channel.id)\n        address = channel.outputs[0].get_address(self.account.ledger)\n        claim = generate_signed_legacy(address, channel.outputs[0])\n        tx = await Transaction.claim_create('example', claim.SerializeToString(), 1, address, [self.account], self.account)\n        await tx.sign([self.account])\n        await self.broadcast_and_confirm(tx)\n\n        response = await self.resolve('@olds/example')\n        self.assertTrue('is_channel_signature_valid' in response, str(response))\n        self.assertTrue(response['is_channel_signature_valid'])\n\n        claim.publisherSignature.signature = bytes(reversed(claim.publisherSignature.signature))\n        tx = await Transaction.claim_create(\n            'bad_example', claim.SerializeToString(), 1, address, [self.account], self.account\n        )\n        await tx.sign([self.account])\n        await self.broadcast_and_confirm(tx)\n\n        response = await self.resolve('bad_example')\n        self.assertFalse(response['is_channel_signature_valid'])\n        self.assertEqual(\n            {'error': {\n                'name': 'NOT_FOUND',\n                'text': 'Could not find claim at \"@olds/bad_example\".',\n            }},\n            await self.resolve('@olds/bad_example')\n        )\n\n    async def test_resolve_with_includes(self):\n        wallet2 = await self.daemon.jsonrpc_wallet_create('wallet2', create_account=True)\n        address2 = await self.daemon.jsonrpc_address_unused(wallet_id=wallet2.id)\n\n        await self.wallet_send('1.0', address2)\n\n        stream = await self.stream_create(\n            'priced', '0.1', wallet_id=wallet2.id,\n            fee_amount='0.5', fee_currency='LBC', fee_address=address2\n        )\n        stream_id = self.get_claim_id(stream)\n\n        resolve = await self.resolve('priced')\n        self.assertNotIn('is_my_output', resolve)\n        self.assertNotIn('purchase_receipt', resolve)\n        self.assertNotIn('sent_supports', resolve)\n        self.assertNotIn('sent_tips', resolve)\n        self.assertNotIn('received_tips', resolve)\n\n        # is_my_output\n        resolve = await self.resolve('priced', include_is_my_output=True)\n        self.assertFalse(resolve['is_my_output'])\n        resolve = await self.resolve('priced', wallet_id=wallet2.id, include_is_my_output=True)\n        self.assertTrue(resolve['is_my_output'])\n\n        # purchase receipt\n        resolve = await self.resolve('priced', include_purchase_receipt=True)\n        self.assertNotIn('purchase_receipt', resolve)\n        await self.purchase_create(stream_id)\n        resolve = await self.resolve('priced', include_purchase_receipt=True)\n        self.assertEqual('0.5', resolve['purchase_receipt']['amount'])\n\n        # my supports and my tips\n        resolve = await self.resolve(\n            'priced', include_sent_supports=True, include_sent_tips=True, include_received_tips=True\n        )\n        self.assertEqual('0.0', resolve['sent_supports'])\n        self.assertEqual('0.0', resolve['sent_tips'])\n        self.assertEqual('0.0', resolve['received_tips'])\n        await self.support_create(stream_id, '0.3')\n        await self.support_create(stream_id, '0.2')\n        await self.support_create(stream_id, '0.4', tip=True)\n        await self.support_create(stream_id, '0.5', tip=True)\n        resolve = await self.resolve(\n            'priced', include_sent_supports=True, include_sent_tips=True, include_received_tips=True\n        )\n        self.assertEqual('0.5', resolve['sent_supports'])\n        self.assertEqual('0.9', resolve['sent_tips'])\n        self.assertEqual('0.0', resolve['received_tips'])\n\n        resolve = await self.resolve(\n            'priced', include_sent_supports=True, include_sent_tips=True, include_received_tips=True,\n            wallet_id=wallet2.id\n        )\n        self.assertEqual('0.0', resolve['sent_supports'])\n        self.assertEqual('0.0', resolve['sent_tips'])\n        self.assertEqual('0.9', resolve['received_tips'])\n        self.assertEqual('1.4', resolve['meta']['support_amount'])\n\n        # make sure nothing is leaked between wallets through cached tx/txos\n        resolve = await self.resolve('priced')\n        self.assertNotIn('is_my_output', resolve)\n        self.assertNotIn('purchase_receipt', resolve)\n        self.assertNotIn('sent_supports', resolve)\n        self.assertNotIn('sent_tips', resolve)\n        self.assertNotIn('received_tips', resolve)\n\n\nclass ResolveClaimTakeovers(BaseResolveTestCase):\n    async def test_channel_invalidation(self):\n        channel_id = (await self.channel_create('@test', '0.1'))['outputs'][0]['claim_id']\n        channel_id2 = (await self.channel_create('@other', '0.1'))['outputs'][0]['claim_id']\n\n        async def make_claim(name, amount, channel_id=None):\n            return (\n            await self.stream_create(name, amount, channel_id=channel_id)\n        )['outputs'][0]['claim_id']\n\n        unsigned_then_signed = await make_claim('unsigned_then_signed', '0.1')\n        unsigned_then_updated_then_signed = await make_claim('unsigned_then_updated_then_signed', '0.1')\n        signed_then_unsigned = await make_claim(\n            'signed_then_unsigned', '0.01',  channel_id=channel_id\n        )\n        signed_then_signed_different_chan = await make_claim(\n            'signed_then_signed_different_chan', '0.01', channel_id=channel_id\n        )\n\n        self.assertIn(\"error\", await self.resolve('@test/unsigned_then_signed'))\n        await self.assertMatchClaimIsWinning('unsigned_then_signed', unsigned_then_signed)\n        self.assertIn(\"error\", await self.resolve('@test/unsigned_then_updated_then_signed'))\n        await self.assertMatchClaimIsWinning('unsigned_then_updated_then_signed', unsigned_then_updated_then_signed)\n        self.assertDictEqual(\n            await self.resolve('@test/signed_then_unsigned'), await self.resolve('signed_then_unsigned')\n        )\n        await self.assertMatchClaimIsWinning('signed_then_unsigned', signed_then_unsigned)\n        # sign 'unsigned_then_signed' and update it\n        await self.ledger.wait(await self.daemon.jsonrpc_stream_update(\n            unsigned_then_signed, '0.09', channel_id=channel_id))\n\n        await self.ledger.wait(await self.daemon.jsonrpc_stream_update(unsigned_then_updated_then_signed, '0.09'))\n        await self.ledger.wait(await self.daemon.jsonrpc_stream_update(\n            unsigned_then_updated_then_signed, '0.09', channel_id=channel_id))\n\n        await self.ledger.wait(await self.daemon.jsonrpc_stream_update(\n            signed_then_unsigned, '0.09', clear_channel=True))\n\n        await self.ledger.wait(await self.daemon.jsonrpc_stream_update(\n            signed_then_signed_different_chan, '0.09', channel_id=channel_id2))\n\n        await self.daemon.jsonrpc_txo_spend(type='channel', claim_id=channel_id)\n\n        signed3 = await make_claim('signed3', '0.01',  channel_id=channel_id)\n        signed4 = await make_claim('signed4', '0.01',  channel_id=channel_id2)\n\n        self.assertIn(\"error\", await self.resolve('@test'))\n        self.assertIn(\"error\", await self.resolve('@test/signed1'))\n        self.assertIn(\"error\", await self.resolve('@test/unsigned_then_updated_then_signed'))\n        self.assertIn(\"error\", await self.resolve('@test/unsigned_then_signed'))\n        self.assertIn(\"error\", await self.resolve('@test/signed3'))\n        self.assertIn(\"error\", await self.resolve('@test/signed4'))\n\n        await self.assertMatchClaimIsWinning('signed_then_unsigned', signed_then_unsigned)\n        await self.assertMatchClaimIsWinning('unsigned_then_signed', unsigned_then_signed)\n        await self.assertMatchClaimIsWinning('unsigned_then_updated_then_signed', unsigned_then_updated_then_signed)\n        await self.assertMatchClaimIsWinning('signed_then_signed_different_chan', signed_then_signed_different_chan)\n        await self.assertMatchClaimIsWinning('signed3', signed3)\n        await self.assertMatchClaimIsWinning('signed4', signed4)\n\n        self.assertDictEqual(await self.resolve('@other/signed_then_signed_different_chan'),\n                             await self.resolve('signed_then_signed_different_chan'))\n        self.assertDictEqual(await self.resolve('@other/signed4'),\n                             await self.resolve('signed4'))\n\n        self.assertEqual(2, len(await self.claim_search(channel_ids=[channel_id2])))\n\n        await self.channel_update(channel_id2)\n        await make_claim('third_signed', '0.01', channel_id=channel_id2)\n        self.assertEqual(3, len(await self.claim_search(channel_ids=[channel_id2])))\n\n    async def _test_activation_delay(self):\n        name = 'derp'\n        # initially claim the name\n        first_claim_id = (await self.stream_create(name, '0.1',  allow_duplicate_name=True))['outputs'][0]['claim_id']\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(320)\n        # a claim of higher amount made now will have a takeover delay of 10\n        second_claim_id = (await self.stream_create(name, '0.2',  allow_duplicate_name=True))['outputs'][0]['claim_id']\n        # sanity check\n        self.assertNotEqual(first_claim_id, second_claim_id)\n        # takeover should not have happened yet\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(9)\n        # not yet\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(1)\n        # the new claim should have activated\n        await self.assertMatchClaimIsWinning(name, second_claim_id)\n        return first_claim_id, second_claim_id\n\n    async def test_activation_delay(self):\n        await self._test_activation_delay()\n\n    async def test_activation_delay_then_abandon_then_reclaim(self):\n        name = 'derp'\n        first_claim_id, second_claim_id = await self._test_activation_delay()\n        await self.daemon.jsonrpc_txo_spend(type='stream', claim_id=first_claim_id)\n        await self.daemon.jsonrpc_txo_spend(type='stream', claim_id=second_claim_id)\n        await self.generate(1)\n        await self.assertNoClaimForName(name)\n        await self._test_activation_delay()\n\n    async def create_stream_claim(self, amount: str, name='derp') -> str:\n        return (await self.stream_create(name, amount,  allow_duplicate_name=True))['outputs'][0]['claim_id']\n\n    async def assertNameState(self, height: int, name: str, winning_claim_id: str, last_takeover_height: int,\n                               non_winning_claims: List[ClaimStateValue]):\n        self.assertEqual(height, self.conductor.spv_node.server.db.db_height)\n        await self.assertMatchClaimIsWinning(name, winning_claim_id)\n        for non_winning in non_winning_claims:\n            claim = await self.assertMatchClaim(name,\n                non_winning.claim_id, is_active_in_lbrycrd=non_winning.active_in_lbrycrd\n            )\n            self.assertEqual(non_winning.activation_height, claim.activation_height)\n            self.assertEqual(last_takeover_height, claim.last_takeover_height)\n\n    async def test_delay_takeover_with_update(self):\n        name = 'derp'\n        first_claim_id = await self.create_stream_claim('0.2', name)\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(320)\n        second_claim_id = await self.create_stream_claim('0.1', name)\n        third_claim_id = await self.create_stream_claim('0.1', name)\n        await self.generate(8)\n        await self.assertNameState(\n            height=537, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=False),\n                ClaimStateValue(third_claim_id, activation_height=539, active_in_lbrycrd=False)\n            ]\n        )\n\n        await self.generate(1)\n        await self.assertNameState(\n            height=538, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=539, active_in_lbrycrd=False)\n            ]\n        )\n\n        await self.generate(1)\n        await self.assertNameState(\n            height=539, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=539, active_in_lbrycrd=True)\n            ]\n        )\n\n        await self.daemon.jsonrpc_stream_update(third_claim_id, '0.21')\n        await self.generate(1)\n        await self.assertNameState(\n            height=540, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=550, active_in_lbrycrd=False)\n            ]\n        )\n\n        await self.generate(9)\n        await self.assertNameState(\n            height=549, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=550, active_in_lbrycrd=False)\n            ]\n        )\n\n        await self.generate(1)\n        await self.assertNameState(\n            height=550, name=name, winning_claim_id=third_claim_id, last_takeover_height=550,\n            non_winning_claims=[\n                ClaimStateValue(first_claim_id, activation_height=207, active_in_lbrycrd=True),\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True)\n            ]\n        )\n\n    async def test_delay_takeover_with_update_then_update_to_lower_before_takeover(self):\n        name = 'derp'\n        first_claim_id = await self.create_stream_claim('0.2', name)\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(320)\n        second_claim_id = await self.create_stream_claim('0.1', name)\n        third_claim_id = await self.create_stream_claim('0.1', name)\n        await self.generate(8)\n        await self.assertNameState(\n            height=537, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=False),\n                ClaimStateValue(third_claim_id, activation_height=539, active_in_lbrycrd=False)\n            ]\n        )\n\n        await self.generate(1)\n        await self.assertNameState(\n            height=538, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=539, active_in_lbrycrd=False)\n            ]\n        )\n\n        await self.generate(1)\n        await self.assertNameState(\n            height=539, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=539, active_in_lbrycrd=True)\n            ]\n        )\n\n        await self.daemon.jsonrpc_stream_update(third_claim_id, '0.21')\n        await self.generate(1)\n        await self.assertNameState(\n            height=540, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=550, active_in_lbrycrd=False)\n            ]\n        )\n\n        await self.generate(8)\n        await self.assertNameState(\n            height=548, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=550, active_in_lbrycrd=False)\n            ]\n        )\n\n        await self.daemon.jsonrpc_stream_update(third_claim_id, '0.09')\n\n        await self.generate(1)\n        await self.assertNameState(\n            height=549, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=559, active_in_lbrycrd=False)\n            ]\n        )\n        await self.generate(10)\n        await self.assertNameState(\n            height=559, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=559, active_in_lbrycrd=True)\n            ]\n        )\n\n    async def test_delay_takeover_with_update_then_update_to_lower_on_takeover(self):\n        name = 'derp'\n        first_claim_id = await self.create_stream_claim('0.2', name)\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(320)\n        second_claim_id = await self.create_stream_claim('0.1', name)\n        third_claim_id = await self.create_stream_claim('0.1', name)\n        await self.generate(8)\n        await self.assertNameState(\n            height=537, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=False),\n                ClaimStateValue(third_claim_id, activation_height=539, active_in_lbrycrd=False)\n            ]\n        )\n\n        await self.generate(1)\n        await self.assertNameState(\n            height=538, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=539, active_in_lbrycrd=False)\n            ]\n        )\n\n        await self.generate(1)\n        await self.assertNameState(\n            height=539, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=539, active_in_lbrycrd=True)\n            ]\n        )\n\n        await self.daemon.jsonrpc_stream_update(third_claim_id, '0.21')\n        await self.generate(1)\n        await self.assertNameState(\n            height=540, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=550, active_in_lbrycrd=False)\n            ]\n        )\n\n        await self.generate(8)\n        await self.assertNameState(\n            height=548, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=550, active_in_lbrycrd=False)\n            ]\n        )\n\n        await self.generate(1)\n        await self.assertNameState(\n            height=549, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=550, active_in_lbrycrd=False)\n            ]\n        )\n\n        await self.daemon.jsonrpc_stream_update(third_claim_id, '0.09')\n        await self.generate(1)\n        await self.assertNameState(\n            height=550, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=560, active_in_lbrycrd=False)\n            ]\n        )\n        await self.generate(10)\n        await self.assertNameState(\n            height=560, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=560, active_in_lbrycrd=True)\n            ]\n        )\n\n    async def test_delay_takeover_with_update_then_update_to_lower_after_takeover(self):\n        name = 'derp'\n        first_claim_id = await self.create_stream_claim('0.2', name)\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(320)\n        second_claim_id = await self.create_stream_claim('0.1', name)\n        third_claim_id = await self.create_stream_claim('0.1', name)\n        await self.generate(8)\n        await self.assertNameState(\n            height=537, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=False),\n                ClaimStateValue(third_claim_id, activation_height=539, active_in_lbrycrd=False)\n            ]\n        )\n        await self.generate(1)\n        await self.assertNameState(\n            height=538, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=539, active_in_lbrycrd=False)\n            ]\n        )\n\n        await self.generate(1)\n        await self.assertNameState(\n            height=539, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=539, active_in_lbrycrd=True)\n            ]\n        )\n\n        await self.daemon.jsonrpc_stream_update(third_claim_id, '0.21')\n        await self.generate(1)\n        await self.assertNameState(\n            height=540, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=550, active_in_lbrycrd=False)\n            ]\n        )\n\n        await self.generate(8)\n        await self.assertNameState(\n            height=548, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=550, active_in_lbrycrd=False)\n            ]\n        )\n\n        await self.generate(1)\n        await self.assertNameState(\n            height=549, name=name, winning_claim_id=first_claim_id, last_takeover_height=207,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=550, active_in_lbrycrd=False)\n            ]\n        )\n\n        await self.generate(1)\n        await self.assertNameState(\n            height=550, name=name, winning_claim_id=third_claim_id, last_takeover_height=550,\n            non_winning_claims=[\n                ClaimStateValue(first_claim_id, activation_height=207, active_in_lbrycrd=True),\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True)\n            ]\n        )\n\n        await self.daemon.jsonrpc_stream_update(third_claim_id, '0.09')\n        await self.generate(1)\n        await self.assertNameState(\n            height=551, name=name, winning_claim_id=first_claim_id, last_takeover_height=551,\n            non_winning_claims=[\n                ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True),\n                ClaimStateValue(third_claim_id, activation_height=551, active_in_lbrycrd=True)\n            ]\n        )\n\n    async def test_resolve_signed_claims_with_fees(self):\n        channel_name = '@abc'\n        channel_id = self.get_claim_id(\n            await self.channel_create(channel_name, '0.01')\n        )\n        self.assertEqual(channel_id, (await self.assertMatchWinningClaim(channel_name)).claim_hash.hex())\n        stream_name = 'foo'\n        stream_with_no_fee = self.get_claim_id(\n            await self.stream_create(stream_name, '0.01', channel_id=channel_id)\n        )\n        stream_with_fee = self.get_claim_id(\n            await self.stream_create('with_a_fee', '0.01', channel_id=channel_id, fee_amount='1', fee_currency='LBC')\n        )\n        greater_than_or_equal_to_zero = [\n            claim['claim_id'] for claim in (\n                await self.conductor.spv_node.server.session_manager.search_index.search(\n                    channel_id=channel_id, fee_amount=\">=0\"\n                ))[0]\n        ]\n        self.assertEqual(2, len(greater_than_or_equal_to_zero))\n        self.assertSetEqual(set(greater_than_or_equal_to_zero), {stream_with_no_fee, stream_with_fee})\n        greater_than_zero = [\n            claim['claim_id'] for claim in (\n                await self.conductor.spv_node.server.session_manager.search_index.search(\n                    channel_id=channel_id, fee_amount=\">0\"\n                ))[0]\n        ]\n        self.assertEqual(1, len(greater_than_zero))\n        self.assertSetEqual(set(greater_than_zero), {stream_with_fee})\n        equal_to_zero = [\n            claim['claim_id'] for claim in (\n                await self.conductor.spv_node.server.session_manager.search_index.search(\n                    channel_id=channel_id, fee_amount=\"<=0\"\n                ))[0]\n        ]\n        self.assertEqual(1, len(equal_to_zero))\n        self.assertSetEqual(set(equal_to_zero), {stream_with_no_fee})\n\n    async def test_spec_example(self):\n        # https://spec.lbry.com/#claim-activation-example\n        # this test has adjusted block heights from the example because it uses the regtest chain instead of mainnet\n        # on regtest, claims expire much faster, so we can't do the ~1000 block delay in the spec example exactly\n\n        name = 'test'\n        await self.generate(494)\n        address = (await self.account.receiving.get_addresses(True))[0]\n        await self.send_to_address_and_wait(address, 400.0)\n        await self.account.ledger.on_address.first\n        await self.generate(100)\n        self.assertEqual(800, self.conductor.spv_node.server.db.db_height)\n\n        # Block 801: Claim A for 10 LBC is accepted.\n        # It is the first claim, so it immediately becomes active and controlling.\n        # State: A(10) is controlling\n        claim_id_A = (await self.stream_create(name, '10.0',  allow_duplicate_name=True))['outputs'][0]['claim_id']\n        await self.assertMatchClaimIsWinning(name, claim_id_A)\n\n        # Block 1121: Claim B for 20 LBC is accepted.\n        # Its activation height is 1121 + min(4032, floor((1121-801) / 32)) = 1121 + 10 = 1131.\n        # State: A(10) is controlling, B(20) is accepted.\n        await self.generate(32 * 10 - 1)\n        self.assertEqual(1120, self.conductor.spv_node.server.db.db_height)\n        claim_id_B = (await self.stream_create(name, '20.0', allow_duplicate_name=True))['outputs'][0]['claim_id']\n        claim_B, _, _, _ = await self.conductor.spv_node.server.db.resolve(f\"{name}:{claim_id_B}\")\n        self.assertEqual(1121, self.conductor.spv_node.server.db.db_height)\n        self.assertEqual(1131, claim_B.activation_height)\n        await self.assertMatchClaimIsWinning(name, claim_id_A)\n\n        # Block 1122: Support X for 14 LBC for claim A is accepted.\n        # Since it is a support for the controlling claim, it activates immediately.\n        # State: A(10+14) is controlling, B(20) is accepted.\n        await self.support_create(claim_id_A, bid='14.0')\n        self.assertEqual(1122, self.conductor.spv_node.server.db.db_height)\n        await self.assertMatchClaimIsWinning(name, claim_id_A)\n\n        # Block 1123: Claim C for 50 LBC is accepted.\n        # The activation height is 1123 + min(4032, floor((1123-801) / 32)) = 1123 + 10 = 1133.\n        # State: A(10+14) is controlling, B(20) is accepted, C(50) is accepted.\n        claim_id_C = (await self.stream_create(name, '50.0', allow_duplicate_name=True))['outputs'][0]['claim_id']\n        self.assertEqual(1123, self.conductor.spv_node.server.db.db_height)\n        claim_C, _, _, _ = await self.conductor.spv_node.server.db.resolve(f\"{name}:{claim_id_C}\")\n        self.assertEqual(1133, claim_C.activation_height)\n        await self.assertMatchClaimIsWinning(name, claim_id_A)\n\n        await self.generate(7)\n        self.assertEqual(1130, self.conductor.spv_node.server.db.db_height)\n        await self.assertMatchClaimIsWinning(name, claim_id_A)\n        await self.generate(1)\n\n        # Block 1131: Claim B activates. It has 20 LBC, while claim A has 24 LBC (10 original + 14 from support X). There is no takeover, and claim A remains controlling.\n        # State: A(10+14) is controlling, B(20) is active, C(50) is accepted.\n        self.assertEqual(1131, self.conductor.spv_node.server.db.db_height)\n        await self.assertMatchClaimIsWinning(name, claim_id_A)\n\n        # Block 1132: Claim D for 300 LBC is accepted. The activation height is 1132 + min(4032, floor((1132-801) / 32)) = 1132 + 10 = 1142.\n        # State: A(10+14) is controlling, B(20) is active, C(50) is accepted, D(300) is accepted.\n        claim_id_D = (await self.stream_create(name, '300.0', allow_duplicate_name=True))['outputs'][0]['claim_id']\n        self.assertEqual(1132, self.conductor.spv_node.server.db.db_height)\n        claim_D, _, _, _ = await self.conductor.spv_node.server.db.resolve(f\"{name}:{claim_id_D}\")\n        self.assertEqual(False, claim_D.is_controlling)\n        self.assertEqual(801, claim_D.last_takeover_height)\n        self.assertEqual(1142, claim_D.activation_height)\n        await self.assertMatchClaimIsWinning(name, claim_id_A)\n\n        # Block 1133: Claim C activates. It has 50 LBC, while claim A has 24 LBC, so a takeover is initiated. The takeover height for this name is set to 1133, and therefore the activation delay for all the claims becomes min(4032, floor((1133-1133) / 32)) = 0. All the claims become active. The totals for each claim are recalculated, and claim D becomes controlling because it has the highest total.\n        # State: A(10+14) is active, B(20) is active, C(50) is active, D(300) is controlling\n        await self.generate(1)\n        self.assertEqual(1133, self.conductor.spv_node.server.db.db_height)\n        claim_D, _, _, _ = await self.conductor.spv_node.server.db.resolve(f\"{name}:{claim_id_D}\")\n        self.assertEqual(True, claim_D.is_controlling)\n        self.assertEqual(1133, claim_D.last_takeover_height)\n        self.assertEqual(1133, claim_D.activation_height)\n        await self.assertMatchClaimIsWinning(name, claim_id_D)\n\n    async def test_early_takeover(self):\n        name = 'derp'\n        # block 207\n        first_claim_id = (await self.stream_create(name, '0.1',  allow_duplicate_name=True))['outputs'][0]['claim_id']\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n\n        await self.generate(96)\n        # block 304, activates at 307\n        second_claim_id = (await self.stream_create(name, '0.2',  allow_duplicate_name=True))['outputs'][0]['claim_id']\n        # block 305, activates at 308 (but gets triggered early by the takeover by the second claim)\n        third_claim_id = (await self.stream_create(name, '0.3',  allow_duplicate_name=True))['outputs'][0]['claim_id']\n        self.assertNotEqual(first_claim_id, second_claim_id)\n        # takeover should not have happened yet\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(1)\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(1)\n        await self.assertMatchClaimIsWinning(name, third_claim_id)\n        await self.generate(1)\n        await self.assertMatchClaimIsWinning(name, third_claim_id)\n\n    async def test_early_takeover_zero_delay(self):\n        name = 'derp'\n        # block 207\n        first_claim_id = (await self.stream_create(name, '0.1',  allow_duplicate_name=True))['outputs'][0]['claim_id']\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n\n        await self.generate(96)\n        # block 304, activates at 307\n        second_claim_id = (await self.stream_create(name, '0.2',  allow_duplicate_name=True))['outputs'][0]['claim_id']\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(1)\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(1)\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        # on block 307 make a third claim with a yet higher amount, it takes over with no delay because the\n        # second claim activates and begins the takeover on this block\n        third_claim_id = (await self.stream_create(name, '0.3',  allow_duplicate_name=True))['outputs'][0]['claim_id']\n        await self.assertMatchClaimIsWinning(name, third_claim_id)\n        await self.generate(1)\n        await self.assertMatchClaimIsWinning(name, third_claim_id)\n\n    async def test_early_takeover_from_support_zero_delay(self):\n        name = 'derp'\n        # block 207\n        first_claim_id = (await self.stream_create(name, '0.1',  allow_duplicate_name=True))['outputs'][0]['claim_id']\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n\n        await self.generate(96)\n        # block 304, activates at 307\n        second_claim_id = (await self.stream_create(name, '0.2',  allow_duplicate_name=True))['outputs'][0]['claim_id']\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(1)\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        third_claim_id = (await self.stream_create(name, '0.19',  allow_duplicate_name=True))['outputs'][0]['claim_id']\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        tx = await self.daemon.jsonrpc_support_create(third_claim_id, '0.1')\n        await self.ledger.wait(tx)\n        await self.generate(1)\n        await self.assertMatchClaimIsWinning(name, third_claim_id)\n        await self.generate(1)\n        await self.assertMatchClaimIsWinning(name, third_claim_id)\n\n    async def test_early_takeover_from_support_and_claim_zero_delay(self):\n        name = 'derp'\n        # block 207\n        first_claim_id = (await self.stream_create(name, '0.1',  allow_duplicate_name=True))['outputs'][0]['claim_id']\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n\n        await self.generate(96)\n        # block 304, activates at 307\n        second_claim_id = (await self.stream_create(name, '0.2',  allow_duplicate_name=True))['outputs'][0]['claim_id']\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(1)\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(1)\n\n        file_path = self.create_upload_file(data=b'hi!')\n        tx = await self.daemon.jsonrpc_stream_create(name, '0.19', file_path=file_path, allow_duplicate_name=True)\n        await self.ledger.wait(tx)\n        third_claim_id = tx.outputs[0].claim_id\n\n        wallet = self.daemon.wallet_manager.get_wallet_or_default(None)\n        funding_accounts = wallet.get_accounts_or_all(None)\n        amount = self.daemon.get_dewies_or_error(\"amount\", '0.1')\n        account = wallet.get_account_or_default(None)\n        claim_address = await account.receiving.get_or_create_usable_address()\n        tx = await Transaction.support(\n            'derp', third_claim_id, amount, claim_address, funding_accounts, funding_accounts[0], None\n        )\n        await tx.sign(funding_accounts)\n        await self.daemon.broadcast_or_release(tx, True)\n        await self.ledger.wait(tx)\n        await self.generate(1)\n        await self.assertMatchClaimIsWinning(name, third_claim_id)\n        await self.generate(1)\n        await self.assertMatchClaimIsWinning(name, third_claim_id)\n\n    async def test_early_takeover_abandoned_controlling_support(self):\n        name = 'derp'\n        # block 207\n        first_claim_id = (await self.stream_create(name, '0.1', allow_duplicate_name=True))['outputs'][0][\n            'claim_id']\n        tx = await self.daemon.jsonrpc_support_create(first_claim_id, '0.2')\n        await self.ledger.wait(tx)\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(96)\n        # block 304, activates at 307\n        second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0][\n            'claim_id']\n        # block 305, activates at 308 (but gets triggered early by the takeover by the second claim)\n        third_claim_id = (await self.stream_create(name, '0.3', allow_duplicate_name=True))['outputs'][0][\n            'claim_id']\n        self.assertNotEqual(first_claim_id, second_claim_id)\n        # takeover should not have happened yet\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(1)\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.daemon.jsonrpc_txo_spend(type='support', txid=tx.id)\n        await self.generate(1)\n        await self.assertMatchClaimIsWinning(name, third_claim_id)\n        await self.generate(1)\n        await self.assertMatchClaimIsWinning(name, third_claim_id)\n\n    async def test_block_takeover_with_delay_1_support(self):\n        name = 'derp'\n        # initially claim the name\n        first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id']\n        self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex())\n        await self.generate(320)\n        # a claim of higher amount made now will have a takeover delay of 10\n        second_claim_id = (await self.stream_create(name, '0.2',  allow_duplicate_name=True))['outputs'][0]['claim_id']\n        # sanity check\n        self.assertNotEqual(first_claim_id, second_claim_id)\n        # takeover should not have happened yet\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        for _ in range(8):\n            await self.generate(1)\n            await self.assertMatchClaimIsWinning(name, first_claim_id)\n        # prevent the takeover by adding a support one block before the takeover happens\n        await self.support_create(first_claim_id, bid='1.0')\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        # one more block until activation\n        await self.generate(1)\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n\n    async def test_block_takeover_with_delay_0_support(self):\n        name = 'derp'\n        # initially claim the name\n        first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id']\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(320)\n        # a claim of higher amount made now will have a takeover delay of 10\n        second_claim_id = (await self.stream_create(name, '0.2',  allow_duplicate_name=True))['outputs'][0]['claim_id']\n        # sanity check\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        # takeover should not have happened yet\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(9)\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        # prevent the takeover by adding a support on the same block the takeover would happen\n        await self.support_create(first_claim_id, bid='1.0')\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n\n    async def _test_almost_prevent_takeover(self, name: str, blocks: int = 9):\n        # initially claim the name\n        first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id']\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(320)\n        # a claim of higher amount made now will have a takeover delay of 10\n        second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id']\n        # sanity check\n        self.assertNotEqual(first_claim_id, second_claim_id)\n        # takeover should not have happened yet\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(blocks)\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        # prevent the takeover by adding a support on the same block the takeover would happen\n        tx = await self.daemon.jsonrpc_support_create(first_claim_id, '1.0')\n        await self.ledger.wait(tx)\n        return first_claim_id, second_claim_id, tx\n\n    async def test_almost_prevent_takeover_remove_support_same_block_supported(self):\n        name = 'derp'\n        first_claim_id, second_claim_id, tx = await self._test_almost_prevent_takeover(name, 9)\n        await self.daemon.jsonrpc_txo_spend(type='support', txid=tx.id)\n        await self.generate(1)\n        await self.assertMatchClaimIsWinning(name, second_claim_id)\n\n    async def test_almost_prevent_takeover_remove_support_one_block_after_supported(self):\n        name = 'derp'\n        first_claim_id, second_claim_id, tx = await self._test_almost_prevent_takeover(name, 8)\n        await self.generate(1)\n        await self.daemon.jsonrpc_txo_spend(type='support', txid=tx.id)\n        await self.generate(1)\n        await self.assertMatchClaimIsWinning(name, second_claim_id)\n\n    async def test_abandon_before_takeover(self):\n        name = 'derp'\n        # initially claim the name\n        first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id']\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(320)\n        # a claim of higher amount made now will have a takeover delay of 10\n        second_claim_id = (await self.stream_create(name, '0.2',  allow_duplicate_name=True))['outputs'][0]['claim_id']\n        # sanity check\n        self.assertNotEqual(first_claim_id, second_claim_id)\n        # takeover should not have happened yet\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(8)\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        # abandon the winning claim\n        await self.daemon.jsonrpc_txo_spend(type='stream', claim_id=first_claim_id)\n        await self.generate(1)\n        # the takeover and activation should happen a block earlier than they would have absent the abandon\n        await self.assertMatchClaimIsWinning(name, second_claim_id)\n        await self.generate(1)\n        await self.assertMatchClaimIsWinning(name, second_claim_id)\n\n    async def test_abandon_before_takeover_no_delay_update(self):  # TODO: fix race condition line 506\n        name = 'derp'\n        # initially claim the name\n        first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id']\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(320)\n        # block 527\n        # a claim of higher amount made now will have a takeover delay of 10\n        second_claim_id = (await self.stream_create(name, '0.2',  allow_duplicate_name=True))['outputs'][0]['claim_id']\n        # block 528\n        # sanity check\n        self.assertNotEqual(first_claim_id, second_claim_id)\n        # takeover should not have happened yet\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.assertMatchClaimsForName(name)\n        await self.generate(8)\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.assertMatchClaimsForName(name)\n        # abandon the winning claim\n        await self.daemon.jsonrpc_txo_spend(type='stream', claim_id=first_claim_id)\n        await self.daemon.jsonrpc_stream_update(second_claim_id, '0.1')\n        await self.generate(1)\n\n        # the takeover and activation should happen a block earlier than they would have absent the abandon\n        await self.assertMatchClaimIsWinning(name, second_claim_id)\n        await self.assertMatchClaimsForName(name)\n        await self.generate(1)\n        # await self.ledger.on_header.where(lambda e: e.height == 537)\n        await self.assertMatchClaimIsWinning(name, second_claim_id)\n        await self.assertMatchClaimsForName(name)\n\n    async def test_abandon_controlling_support_before_pending_takeover(self):\n        name = 'derp'\n        # initially claim the name\n        first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id']\n        controlling_support_tx = await self.daemon.jsonrpc_support_create(first_claim_id, '0.9')\n        await self.ledger.wait(controlling_support_tx)\n        self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex())\n        await self.generate(321)\n\n        second_claim_id = (await self.stream_create(name, '0.9',  allow_duplicate_name=True))['outputs'][0]['claim_id']\n\n        self.assertNotEqual(first_claim_id, second_claim_id)\n        # takeover should not have happened yet\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(8)\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        # abandon the support that causes the winning claim to have the highest staked\n        tx = await self.daemon.jsonrpc_txo_spend(type='support', txid=controlling_support_tx.id, blocking=True)\n        await self.generate(1)\n        await self.assertNameState(538, name, first_claim_id, last_takeover_height=207, non_winning_claims=[\n            ClaimStateValue(second_claim_id, activation_height=539, active_in_lbrycrd=False)\n        ])\n        await self.generate(1)\n        await self.assertNameState(539, name, second_claim_id, last_takeover_height=539, non_winning_claims=[\n            ClaimStateValue(first_claim_id, activation_height=207, active_in_lbrycrd=True)\n        ])\n\n    async def test_remove_controlling_support(self):\n        name = 'derp'\n        # initially claim the name\n        first_claim_id = (await self.stream_create(name, '0.2'))['outputs'][0]['claim_id']\n        first_support_tx = await self.daemon.jsonrpc_support_create(first_claim_id, '0.9')\n        await self.ledger.wait(first_support_tx)\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(320)  # give the first claim long enough for a 10 block takeover delay\n        await self.assertNameState(527, name, first_claim_id, last_takeover_height=207, non_winning_claims=[])\n\n        # make a second claim which will take over the name\n        second_claim_id = (await self.stream_create(name, '0.1',  allow_duplicate_name=True))['outputs'][0]['claim_id']\n        await self.assertNameState(528, name, first_claim_id, last_takeover_height=207, non_winning_claims=[\n            ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=False)\n        ])\n\n        second_claim_support_tx = await self.daemon.jsonrpc_support_create(second_claim_id, '1.5')\n        await self.ledger.wait(second_claim_support_tx)\n        await self.generate(1)  # neither the second claim or its support have activated yet\n        await self.assertNameState(529, name, first_claim_id, last_takeover_height=207, non_winning_claims=[\n            ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=False)\n        ])\n        await self.generate(9)  # claim activates, but is not yet winning\n        await self.assertNameState(538, name, first_claim_id, last_takeover_height=207, non_winning_claims=[\n            ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True)\n        ])\n        await self.generate(1)  # support activates, takeover happens\n        await self.assertNameState(539, name, second_claim_id, last_takeover_height=539, non_winning_claims=[\n            ClaimStateValue(first_claim_id, activation_height=207, active_in_lbrycrd=True)\n        ])\n\n        await self.daemon.jsonrpc_txo_spend(type='support', claim_id=second_claim_id, blocking=True)\n        await self.generate(1)  # support activates, takeover happens\n        await self.assertNameState(540, name, first_claim_id, last_takeover_height=540, non_winning_claims=[\n            ClaimStateValue(second_claim_id, activation_height=538, active_in_lbrycrd=True)\n        ])\n\n    async def test_claim_expiration(self):\n        name = 'derp'\n        # starts at height 206\n        vanishing_claim = (await self.stream_create('vanish', '0.1'))['outputs'][0]['claim_id']\n\n        await self.generate(493)\n        # in block 701 and 702\n        first_claim_id = (await self.stream_create(name, '0.3'))['outputs'][0]['claim_id']\n        await self.assertMatchClaimIsWinning('vanish', vanishing_claim)\n        await self.generate(100)  # block 801, expiration fork happened\n        await self.assertNoClaimForName('vanish')\n        # second claim is in block 802\n        second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id']\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(498)\n        await self.assertMatchClaimIsWinning(name, first_claim_id)\n        await self.generate(1)\n        await self.assertMatchClaimIsWinning(name, second_claim_id)\n        await self.generate(100)\n        await self.assertMatchClaimIsWinning(name, second_claim_id)\n        await self.generate(1)\n        await self.assertNoClaimForName(name)\n\n    async def _test_add_non_winning_already_claimed(self):\n        name = 'derp'\n        # initially claim the name\n        first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id']\n        self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex())\n        await self.generate(32)\n\n        second_claim_id = (await self.stream_create(name, '0.01', allow_duplicate_name=True))['outputs'][0]['claim_id']\n        await self.assertNoClaim(name, second_claim_id)\n        self.assertEqual(\n            len((await self.conductor.spv_node.server.session_manager.search_index.search(claim_name=name))[0]), 1\n        )\n        await self.generate(1)\n        await self.assertMatchClaim(name, second_claim_id)\n        self.assertEqual(\n            len((await self.conductor.spv_node.server.session_manager.search_index.search(claim_name=name))[0]), 2\n        )\n\n    async def test_abandon_controlling_same_block_as_new_claim(self):\n        name = 'derp'\n\n        first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id']\n        await self.generate(64)\n        await self.assertNameState(271, name, first_claim_id, last_takeover_height=207, non_winning_claims=[])\n\n        await self.daemon.jsonrpc_txo_spend(type='stream', claim_id=first_claim_id)\n        second_claim_id = (await self.stream_create(name, '0.1', allow_duplicate_name=True))['outputs'][0]['claim_id']\n        await self.assertNameState(272, name, second_claim_id, last_takeover_height=272, non_winning_claims=[])\n\n    async def test_trending(self):\n        async def get_trending_score(claim_id):\n            return (await self.conductor.spv_node.server.session_manager.search_index.search(\n                claim_id=claim_id\n            ))[0][0]['trending_score']\n\n        claim_id1 = (await self.stream_create('derp', '1.0'))['outputs'][0]['claim_id']\n        COIN = int(1E8)\n\n        self.assertEqual(self.conductor.spv_node.writer.height, 207)\n        self.conductor.spv_node.writer.db.prefix_db.trending_notification.stash_put(\n            (208, bytes.fromhex(claim_id1)), (0, 10 * COIN)\n        )\n        await self.generate(1)\n        self.assertEqual(self.conductor.spv_node.writer.height, 208)\n\n        self.assertEqual(1.7090807854206793, await get_trending_score(claim_id1))\n        self.conductor.spv_node.writer.db.prefix_db.trending_notification.stash_put(\n            (209, bytes.fromhex(claim_id1)), (10 * COIN, 100 * COIN)\n        )\n        await self.generate(1)\n        self.assertEqual(self.conductor.spv_node.writer.height, 209)\n        self.assertEqual(2.2437974397778886, await get_trending_score(claim_id1))\n        self.conductor.spv_node.writer.db.prefix_db.trending_notification.stash_put(\n            (309, bytes.fromhex(claim_id1)), (100 * COIN, 1000000 * COIN)\n        )\n        await self.generate(100)\n        self.assertEqual(self.conductor.spv_node.writer.height, 309)\n        self.assertEqual(5.157053472135866, await get_trending_score(claim_id1))\n\n        self.conductor.spv_node.writer.db.prefix_db.trending_notification.stash_put(\n            (409, bytes.fromhex(claim_id1)), (1000000 * COIN, 1 * COIN)\n        )\n\n        await self.generate(99)\n        self.assertEqual(self.conductor.spv_node.writer.height, 408)\n        self.assertEqual(5.157053472135866, await get_trending_score(claim_id1))\n\n        await self.generate(1)\n        self.assertEqual(self.conductor.spv_node.writer.height, 409)\n\n        self.assertEqual(-3.4256156592205627, await get_trending_score(claim_id1))\n        search_results = (await self.conductor.spv_node.server.session_manager.search_index.search(claim_name=\"derp\"))[0]\n        self.assertEqual(1, len(search_results))\n        self.assertListEqual([claim_id1], [c['claim_id'] for c in search_results])\n\n\nclass ResolveAfterReorg(BaseResolveTestCase):\n    async def reorg(self, start):\n        blocks = self.ledger.headers.height - start\n        self.blockchain.block_expected = start - 1\n\n        # go back to start\n        await self.blockchain.invalidate_block((await self.ledger.headers.hash(start)).decode())\n        # go to previous + 1\n        await self.generate(blocks + 2)\n\n    async def assertBlockHash(self, height):\n        reader_db = self.conductor.spv_node.server.db\n        block_hash = await self.blockchain.get_block_hash(height)\n\n        self.assertEqual(block_hash, (await self.ledger.headers.hash(height)).decode())\n        self.assertEqual(block_hash, (await reader_db.fs_block_hashes(height, 1))[0][::-1].hex())\n        txids = [\n            tx_hash[::-1].hex() for tx_hash in reader_db.get_block_txs(height)\n        ]\n        txs = await reader_db.get_transactions_and_merkles(txids)\n        block_txs = (await self.conductor.spv_node.server.daemon.deserialised_block(block_hash))['tx']\n        self.assertSetEqual(set(block_txs), set(txs.keys()), msg='leveldb/lbrycrd is missing transactions')\n        self.assertListEqual(block_txs, list(txs.keys()), msg='leveldb/lbrycrd transactions are of order')\n\n    async def test_reorg(self):\n        self.assertEqual(self.ledger.headers.height, 206)\n\n        channel_name = '@abc'\n        channel_id = self.get_claim_id(\n            await self.channel_create(channel_name, '0.01')\n        )\n\n        await self.assertNameState(\n            height=207, name='@abc', winning_claim_id=channel_id, last_takeover_height=207,\n            non_winning_claims=[]\n        )\n\n        await self.reorg(206)\n\n        await self.assertNameState(\n            height=208, name='@abc', winning_claim_id=channel_id, last_takeover_height=207,\n            non_winning_claims=[]\n        )\n\n        # await self.assertNoClaimForName(channel_name)\n        # self.assertNotIn('error', await self.resolve(channel_name))\n\n        stream_name = 'foo'\n        stream_id = self.get_claim_id(\n            await self.stream_create(stream_name, '0.01', channel_id=channel_id)\n        )\n\n        await self.assertNameState(\n            height=209, name=stream_name, winning_claim_id=stream_id, last_takeover_height=209,\n            non_winning_claims=[]\n        )\n        await self.reorg(206)\n        await self.assertNameState(\n            height=210, name=stream_name, winning_claim_id=stream_id, last_takeover_height=209,\n            non_winning_claims=[]\n        )\n\n        await self.support_create(stream_id, '0.01')\n\n        await self.assertNameState(\n            height=211, name=stream_name, winning_claim_id=stream_id, last_takeover_height=209,\n            non_winning_claims=[]\n        )\n        await self.reorg(206)\n        # self.assertNotIn('error', await self.resolve(stream_name))\n        await self.assertNameState(\n            height=212, name=stream_name, winning_claim_id=stream_id, last_takeover_height=209,\n            non_winning_claims=[]\n        )\n\n        await self.stream_abandon(stream_id)\n        self.assertNotIn('error', await self.resolve(channel_name))\n        self.assertIn('error', await self.resolve(stream_name))\n        self.assertEqual(channel_id, (await self.assertMatchWinningClaim(channel_name)).claim_hash.hex())\n        await self.assertNoClaimForName(stream_name)\n        # TODO: check @abc/foo too\n\n        await self.reorg(206)\n        self.assertNotIn('error', await self.resolve(channel_name))\n        self.assertIn('error', await self.resolve(stream_name))\n        self.assertEqual(channel_id, (await self.assertMatchWinningClaim(channel_name)).claim_hash.hex())\n        await self.assertNoClaimForName(stream_name)\n\n        await self.channel_abandon(channel_id)\n        self.assertIn('error', await self.resolve(channel_name))\n        self.assertIn('error', await self.resolve(stream_name))\n        await self.reorg(206)\n        self.assertIn('error', await self.resolve(channel_name))\n        self.assertIn('error', await self.resolve(stream_name))\n\n    async def test_reorg_change_claim_height(self):\n        # sanity check\n        result = await self.resolve('hovercraft')  # TODO: do these for claim_search and resolve both\n        self.assertIn('error', result)\n\n        still_valid = await self.daemon.jsonrpc_stream_create(\n            'still-valid', '1.0', file_path=self.create_upload_file(data=b'hi!')\n        )\n        await self.ledger.wait(still_valid)\n        await self.generate(1)\n        # create a claim and verify it's returned by claim_search\n        self.assertEqual(self.ledger.headers.height, 207)\n        await self.assertBlockHash(207)\n\n        broadcast_tx = await self.daemon.jsonrpc_stream_create(\n            'hovercraft', '1.0', file_path=self.create_upload_file(data=b'hi!')\n        )\n        await self.ledger.wait(broadcast_tx)\n        await self.support_create(still_valid.outputs[0].claim_id, '0.01')\n\n        await self.ledger.wait(broadcast_tx, self.blockchain.block_expected)\n        self.assertEqual(self.ledger.headers.height, 208)\n        await self.assertBlockHash(208)\n\n        claim = await self.resolve('hovercraft')\n        self.assertEqual(claim['txid'], broadcast_tx.id)\n        self.assertEqual(claim['height'], 208)\n\n        # check that our tx is in block 208 as returned by lbrycrdd\n        invalidated_block_hash = (await self.ledger.headers.hash(208)).decode()\n        block_207 = await self.blockchain.get_block(invalidated_block_hash)\n        self.assertIn(claim['txid'], block_207['tx'])\n        self.assertEqual(208, claim['height'])\n\n        # reorg the last block dropping our claim tx\n        await self.blockchain.invalidate_block(invalidated_block_hash)\n        await self.conductor.clear_mempool()\n        await self.blockchain.generate(2)\n\n        # wait for the client to catch up and verify the reorg\n        await asyncio.wait_for(self.on_header(209), 3.0)\n        await self.assertBlockHash(207)\n        await self.assertBlockHash(208)\n        await self.assertBlockHash(209)\n\n        # verify the claim was dropped from block 208 as returned by lbrycrdd\n        reorg_block_hash = await self.blockchain.get_block_hash(208)\n        self.assertNotEqual(invalidated_block_hash, reorg_block_hash)\n        block_207 = await self.blockchain.get_block(reorg_block_hash)\n        self.assertNotIn(claim['txid'], block_207['tx'])\n\n        client_reorg_block_hash = (await self.ledger.headers.hash(208)).decode()\n        self.assertEqual(client_reorg_block_hash, reorg_block_hash)\n\n        # verify the dropped claim is no longer returned by claim search\n        self.assertDictEqual(\n            {'error': {'name': 'NOT_FOUND', 'text': 'Could not find claim at \"hovercraft\".'}},\n            await self.resolve('hovercraft')\n        )\n\n        # verify the claim published a block earlier wasn't also reverted\n        self.assertEqual(207, (await self.resolve('still-valid'))['height'])\n\n        # broadcast the claim in a different block\n        new_txid = await self.blockchain.sendrawtransaction(hexlify(broadcast_tx.raw).decode())\n        self.assertEqual(broadcast_tx.id, new_txid)\n        await self.blockchain.generate(1)\n\n        # wait for the client to catch up\n        await asyncio.wait_for(self.on_header(210), 3.0)\n\n        # verify the claim is in the new block and that it is returned by claim_search\n        republished = await self.resolve('hovercraft')\n        self.assertEqual(210, republished['height'])\n        self.assertEqual(claim['claim_id'], republished['claim_id'])\n\n        # this should still be unchanged\n        self.assertEqual(207, (await self.resolve('still-valid'))['height'])\n\n    async def test_reorg_drop_claim(self):\n        # sanity check\n        result = await self.resolve('hovercraft')  # TODO: do these for claim_search and resolve both\n        self.assertIn('error', result)\n\n        still_valid = await self.daemon.jsonrpc_stream_create(\n            'still-valid', '1.0', file_path=self.create_upload_file(data=b'hi!')\n        )\n        await self.ledger.wait(still_valid)\n        await self.generate(1)\n\n        # create a claim and verify it's returned by claim_search\n        self.assertEqual(self.ledger.headers.height, 207)\n        await self.assertBlockHash(207)\n\n        broadcast_tx = await self.daemon.jsonrpc_stream_create(\n            'hovercraft', '1.0', file_path=self.create_upload_file(data=b'hi!')\n        )\n        await self.ledger.wait(broadcast_tx)\n        await self.generate(1)\n        await self.ledger.wait(broadcast_tx, self.blockchain.block_expected)\n        self.assertEqual(self.ledger.headers.height, 208)\n        await self.assertBlockHash(208)\n\n        claim = await self.resolve('hovercraft')\n        self.assertEqual(claim['txid'], broadcast_tx.id)\n        self.assertEqual(claim['height'], 208)\n\n        # check that our tx is in block 208 as returned by lbrycrdd\n        invalidated_block_hash = (await self.ledger.headers.hash(208)).decode()\n        block_207 = await self.blockchain.get_block(invalidated_block_hash)\n        self.assertIn(claim['txid'], block_207['tx'])\n        self.assertEqual(208, claim['height'])\n\n        # reorg the last block dropping our claim tx\n        await self.blockchain.invalidate_block(invalidated_block_hash)\n        await self.conductor.clear_mempool()\n        await self.blockchain.generate(2)\n\n        # wait for the client to catch up and verify the reorg\n        await asyncio.wait_for(self.on_header(209), 30.0)\n        await self.assertBlockHash(207)\n        await self.assertBlockHash(208)\n        await self.assertBlockHash(209)\n\n        # verify the claim was dropped from block 208 as returned by lbrycrdd\n        reorg_block_hash = await self.blockchain.get_block_hash(208)\n        self.assertNotEqual(invalidated_block_hash, reorg_block_hash)\n        block_207 = await self.blockchain.get_block(reorg_block_hash)\n        self.assertNotIn(claim['txid'], block_207['tx'])\n\n        client_reorg_block_hash = (await self.ledger.headers.hash(208)).decode()\n        self.assertEqual(client_reorg_block_hash, reorg_block_hash)\n\n        # verify the dropped claim is no longer returned by claim search\n        self.assertDictEqual(\n            {'error': {'name': 'NOT_FOUND', 'text': 'Could not find claim at \"hovercraft\".'}},\n            await self.resolve('hovercraft')\n        )\n\n        # verify the claim published a block earlier wasn't also reverted\n        self.assertEqual(207, (await self.resolve('still-valid'))['height'])\n\n        # broadcast the claim in a different block\n        new_txid = await self.blockchain.sendrawtransaction(hexlify(broadcast_tx.raw).decode())\n        self.assertEqual(broadcast_tx.id, new_txid)\n        await self.blockchain.generate(1)\n\n        # wait for the client to catch up\n        await asyncio.wait_for(self.on_header(210), 1.0)\n\n        # verify the claim is in the new block and that it is returned by claim_search\n        republished = await self.resolve('hovercraft')\n        self.assertEqual(210, republished['height'])\n        self.assertEqual(claim['claim_id'], republished['claim_id'])\n\n        # this should still be unchanged\n        self.assertEqual(207, (await self.resolve('still-valid'))['height'])\n\n\ndef generate_signed_legacy(address: bytes, output: Output):\n    decoded_address = Base58.decode(address)\n    claim = OldClaimMessage()\n    claim.ParseFromString(unhexlify(\n        '080110011aee04080112a604080410011a2b4865726520617265203520526561736f6e73204920e29da4e'\n        'fb88f204e657874636c6f7564207c20544c4722920346696e64206f7574206d6f72652061626f7574204e'\n        '657874636c6f75643a2068747470733a2f2f6e657874636c6f75642e636f6d2f0a0a596f752063616e206'\n        '6696e64206d65206f6e20746865736520736f6369616c733a0a202a20466f72756d733a2068747470733a'\n        '2f2f666f72756d2e6865617679656c656d656e742e696f2f0a202a20506f64636173743a2068747470733'\n        'a2f2f6f6666746f706963616c2e6e65740a202a2050617472656f6e3a2068747470733a2f2f7061747265'\n        '6f6e2e636f6d2f7468656c696e757867616d65720a202a204d657263683a2068747470733a2f2f7465657'\n        '37072696e672e636f6d2f73746f7265732f6f6666696369616c2d6c696e75782d67616d65720a202a2054'\n        '77697463683a2068747470733a2f2f7477697463682e74762f786f6e64616b0a202a20547769747465723'\n        'a2068747470733a2f2f747769747465722e636f6d2f7468656c696e757867616d65720a0a2e2e2e0a6874'\n        '7470733a2f2f7777772e796f75747562652e636f6d2f77617463683f763d4672546442434f535f66632a0'\n        'f546865204c696e75782047616d6572321c436f7079726967687465642028636f6e746163742061757468'\n        '6f722938004a2968747470733a2f2f6265726b2e6e696e6a612f7468756d626e61696c732f46725464424'\n        '34f535f666352005a001a41080110011a30040e8ac6e89c061f982528c23ad33829fd7146435bf7a4cc22'\n        'f0bff70c4fe0b91fd36da9a375e3e1c171db825bf5d1f32209766964656f2f6d70342a5c080110031a406'\n        '2b2dd4c45e364030fbfad1a6fefff695ebf20ea33a5381b947753e2a0ca359989a5cc7d15e5392a0d354c'\n        '0b68498382b2701b22c03beb8dcb91089031b871e72214feb61536c007cdf4faeeaab4876cb397feaf6b51'\n    ))\n    claim.ClearField(\"publisherSignature\")\n    digest = sha256(b''.join([\n        decoded_address,\n        claim.SerializeToString(),\n        output.claim_hash[::-1]\n    ]))\n    signature = output.private_key.sign_compact(digest)\n    claim.publisherSignature.version = 1\n    claim.publisherSignature.signatureType = 1\n    claim.publisherSignature.signature = signature\n    claim.publisherSignature.certificateId = output.claim_hash[::-1]\n    return claim\n"
  },
  {
    "path": "tests/integration/transactions/__init__.py",
    "content": ""
  },
  {
    "path": "tests/integration/transactions/test_internal_transaction_api.py",
    "content": "import asyncio\n\nfrom lbry.testcase import IntegrationTestCase\n\nimport lbry.wallet\nfrom lbry.schema.claim import Claim\nfrom lbry.wallet.transaction import Transaction, Output, Input\nfrom lbry.wallet.dewies import dewies_to_lbc as d2l, lbc_to_dewies as l2d\n\n\nclass BasicTransactionTest(IntegrationTestCase):\n\n    LEDGER = lbry.wallet\n\n    async def test_creating_updating_and_abandoning_claim_with_channel(self):\n\n        await self.account.ensure_address_gap()\n\n        address1, address2 = await self.account.receiving.get_addresses(limit=2, only_usable=True)\n        notifications = asyncio.create_task(asyncio.wait(\n            [asyncio.ensure_future(self.on_address_update(address1)),\n             asyncio.ensure_future(self.on_address_update(address2))]\n        ))\n        await self.send_to_address_and_wait(address1, 5)\n        await self.send_to_address_and_wait(address2, 5, 1)\n        await notifications\n\n        self.assertEqual(d2l(await self.account.get_balance()), '10.0')\n\n        channel = Claim()\n        channel_txo = Output.pay_claim_name_pubkey_hash(\n            l2d('1.0'), '@bar', channel, self.account.ledger.address_to_hash160(address1)\n        )\n        channel_txo.set_channel_private_key(\n            await self.account.generate_channel_private_key()\n        )\n        channel_txo.script.generate()\n        channel_tx = await Transaction.create([], [channel_txo], [self.account], self.account)\n\n        stream = Claim()\n        stream.stream.source.media_type = \"video/mp4\"\n        stream_txo = Output.pay_claim_name_pubkey_hash(\n            l2d('1.0'), 'foo', stream, self.account.ledger.address_to_hash160(address1)\n        )\n        stream_tx = await Transaction.create([], [stream_txo], [self.account], self.account)\n        stream_txo.sign(channel_txo)\n        await stream_tx.sign([self.account])\n\n        notifications = asyncio.create_task(asyncio.wait(\n            [asyncio.ensure_future(self.ledger.wait(channel_tx)), asyncio.ensure_future(self.ledger.wait(stream_tx))]\n        ))\n\n        await self.broadcast(channel_tx)\n        await self.broadcast(stream_tx)\n        await notifications\n        notifications = asyncio.create_task(asyncio.wait(\n            [asyncio.ensure_future(self.ledger.wait(channel_tx)), asyncio.ensure_future(self.ledger.wait(stream_tx))]\n        ))\n        await self.generate(1)\n        await notifications\n        self.assertEqual(d2l(await self.account.get_balance()), '7.985786')\n        self.assertEqual(d2l(await self.account.get_balance(include_claims=True)), '9.985786')\n\n        response = await self.ledger.resolve([], ['lbry://@bar/foo'])\n        self.assertEqual(response['lbry://@bar/foo'].claim.claim_type, 'stream')\n\n        abandon_tx = await Transaction.create([Input.spend(stream_tx.outputs[0])], [], [self.account], self.account)\n        notify = asyncio.create_task(self.ledger.wait(abandon_tx))\n        await self.broadcast(abandon_tx)\n        await notify\n        notify = asyncio.create_task(self.ledger.wait(abandon_tx))\n        await self.generate(1)\n        await notify\n\n        response = await self.ledger.resolve([], ['lbry://@bar/foo'])\n        self.assertIn('error', response['lbry://@bar/foo'])\n\n        # checks for expected format in inexistent URIs\n        response = await self.ledger.resolve([], ['lbry://404', 'lbry://@404', 'lbry://@404/404'])\n        self.assertEqual('Could not find claim at \"lbry://404\".', response['lbry://404']['error']['text'])\n        self.assertEqual('Could not find channel in \"lbry://@404\".', response['lbry://@404']['error']['text'])\n        self.assertEqual('Could not find channel in \"lbry://@404/404\".', response['lbry://@404/404']['error']['text'])\n"
  },
  {
    "path": "tests/integration/transactions/test_transaction_commands.py",
    "content": "import asyncio\nimport unittest\n\nfrom lbry.testcase import CommandTestCase\nfrom lbry.wallet import Transaction\n\nclass TransactionCommandsTestCase(CommandTestCase):\n\n    async def test_txo_dust_prevention(self):\n        address = await self.daemon.jsonrpc_address_unused(self.account.id)\n        tx = await self.account_send('9.9997758', address)\n        # dust prevention threshold not reached, small txo created\n        self.assertEqual(2, len(tx['outputs']))\n        self.assertEqual(tx['outputs'][1]['amount'], '0.0001002')\n        tx = await self.account_send('9.999706', address)\n        # dust prevention prevented dust\n        self.assertEqual(1, len(tx['outputs']))\n        self.assertEqual(tx['outputs'][0]['amount'], '9.999706')\n\n    async def test_transaction_show(self):\n        # local tx\n        result = await self.out(self.daemon.jsonrpc_account_send(\n            '5.0', await self.daemon.jsonrpc_address_unused(self.account.id), blocking=True\n        ))\n        await self.confirm_tx(result['txid'])\n        tx = await self.daemon.jsonrpc_transaction_show(result['txid'])\n        self.assertEqual(tx.id, result['txid'])\n\n        # someone's tx\n        change_address = await self.blockchain.get_raw_change_address()\n        sendtxid = await self.blockchain.send_to_address(change_address, 10)\n        # After a few tries, Hub should have the transaction (in mempool).\n        for i in range(5):\n            tx = await self.daemon.jsonrpc_transaction_show(sendtxid)\n            # Retry if Hub is not aware of the transaction.\n            if isinstance(tx, dict):\n                # Fields: 'success', 'code', 'message'\n                self.assertFalse(tx['success'], tx)\n                self.assertEqual(tx['code'], 404, tx)\n                self.assertEqual(tx['message'], \"transaction not found\", tx)\n                await asyncio.sleep(0.1)\n                continue\n            break\n        # verify transaction show (in mempool)\n        self.assertTrue(isinstance(tx, Transaction), str(tx))\n        # Fields: 'txid', 'raw', 'height', 'position', 'is_verified', and more.\n        self.assertEqual(tx.id, sendtxid, vars(tx))\n        self.assertEqual(tx.height, -1, vars(tx))\n        self.assertEqual(tx.is_verified, False, vars(tx))\n\n        # transaction is confirmed and leaves mempool\n        await self.generate(1)\n\n        # verify transaction show\n        tx = await self.daemon.jsonrpc_transaction_show(sendtxid)\n        self.assertTrue(isinstance(tx, Transaction), str(tx))\n        self.assertEqual(tx.id, sendtxid, vars(tx))\n        self.assertEqual(tx.height, self.ledger.headers.height, vars(tx))\n        self.assertEqual(tx.is_verified, True, vars(tx))\n\n        # inexistent\n        result = await self.daemon.jsonrpc_transaction_show('0'*64)\n        self.assertTrue(isinstance(result, dict), result)\n        # Fields: 'success', 'code', 'message'\n        self.assertFalse(result['success'], result)\n        self.assertEqual(result['code'], 404, result)\n        self.assertEqual(result['message'], \"transaction not found\", result)\n\n    async def test_utxo_release(self):\n        await self.send_to_address_and_wait(\n            await self.account.receiving.get_or_create_usable_address(), 1, 1\n        )\n        await self.assertBalance(self.account, '11.0')\n        await self.ledger.reserve_outputs(await self.account.get_utxos())\n        await self.assertBalance(self.account, '0.0')\n        await self.daemon.jsonrpc_utxo_release()\n        await self.assertBalance(self.account, '11.0')\n\n\nclass TestSegwit(CommandTestCase):\n\n    @unittest.SkipTest\n    async def test_segwit(self):\n        p2sh_address1 = await self.blockchain.get_new_address(self.blockchain.P2SH_SEGWIT_ADDRESS)\n        p2sh_address2 = await self.blockchain.get_new_address(self.blockchain.P2SH_SEGWIT_ADDRESS)\n        p2sh_address3 = await self.blockchain.get_new_address(self.blockchain.P2SH_SEGWIT_ADDRESS)\n        bech32_address1 = await self.blockchain.get_new_address(self.blockchain.BECH32_ADDRESS)\n        bech32_address2 = await self.blockchain.get_new_address(self.blockchain.BECH32_ADDRESS)\n        bech32_address3 = await self.blockchain.get_new_address(self.blockchain.BECH32_ADDRESS)\n\n        # fund specific addresses for later use\n        p2sh_txid1 = await self.blockchain.send_to_address(p2sh_address1, '1.0')\n        p2sh_txid2 = await self.blockchain.send_to_address(p2sh_address2, '1.0')\n        bech32_txid1 = await self.blockchain.send_to_address(bech32_address1, '1.0')\n        bech32_txid2 = await self.blockchain.send_to_address(bech32_address2, '1.0')\n        await self.generate(1)\n\n        # P2SH & BECH32 can pay to P2SH address\n        tx = await self.blockchain.create_raw_transaction([\n                {\"txid\": p2sh_txid1, \"vout\": 0},\n                {\"txid\": bech32_txid1, \"vout\": 0},\n            ], {p2sh_address3: 1.9}\n        )\n        tx = await self.blockchain.sign_raw_transaction_with_wallet(tx)\n        p2sh_txid3 = await self.blockchain.send_raw_transaction(tx)\n\n        await self.generate(1)\n\n        # P2SH & BECH32 can pay to BECH32 address\n        tx = await self.blockchain.create_raw_transaction([\n            {\"txid\": p2sh_txid2, \"vout\": 0},\n            {\"txid\": bech32_txid2, \"vout\": 0},\n        ], {bech32_address3: 1.9}\n        )\n        tx = await self.blockchain.sign_raw_transaction_with_wallet(tx)\n        bech32_txid3 = await self.blockchain.send_raw_transaction(tx)\n\n        await self.generate(1)\n\n        # P2SH & BECH32 can pay lbry wallet P2PKH\n        address = (await self.account.receiving.get_addresses(limit=1, only_usable=True))[0]\n        tx = await self.blockchain.create_raw_transaction([\n                {\"txid\": p2sh_txid3, \"vout\": 0},\n                {\"txid\": bech32_txid3, \"vout\": 0},\n            ], {address: 3.5}\n        )\n        tx = await self.blockchain.sign_raw_transaction_with_wallet(tx)\n        txid = await self.blockchain.send_raw_transaction(tx)\n        await self.generate_and_wait(1, [txid])\n        await self.assertBalance(self.account, '13.5')\n"
  },
  {
    "path": "tests/integration/transactions/test_transactions.py",
    "content": "import asyncio\nimport random\n\nimport lbry.wallet.rpc.jsonrpc\nfrom lbry.wallet.transaction import Transaction, Output, Input\nfrom lbry.testcase import IntegrationTestCase\nfrom lbry.wallet.util import satoshis_to_coins, coins_to_satoshis\nfrom lbry.wallet.manager import WalletManager\n\n\nclass BasicTransactionTests(IntegrationTestCase):\n    async def test_variety_of_transactions_and_longish_history(self):\n        await self.generate(300)\n        await self.assertBalance(self.account, '0.0')\n        addresses = await self.account.receiving.get_addresses()\n\n        # send 10 coins to first 10 receiving addresses and then 10 transactions worth 10 coins each\n        # to the 10th receiving address for a total of 30 UTXOs on the entire account\n        for i in range(10):\n            notification = asyncio.ensure_future(self.on_address_update(addresses[i]))\n            _ = await self.send_to_address_and_wait(addresses[i], 10)\n            await notification\n            notification = asyncio.ensure_future(self.on_address_update(addresses[9]))\n            _ = await self.send_to_address_and_wait(addresses[9], 10)\n            await notification\n\n        # use batching to reduce issues with send_to_address on cli\n        await self.assertBalance(self.account, '200.0')\n        self.assertEqual(20, await self.account.get_utxo_count())\n\n        # address gap should have increase by 10 to cover the first 10 addresses we've used up\n        addresses = await self.account.receiving.get_addresses()\n        self.assertEqual(30, len(addresses))\n\n        # there used to be a sync bug which failed to save TXIs between\n        # daemon restarts, clearing cache replicates that behavior\n        self.ledger._tx_cache.clear()\n\n        # spend from each of the first 10 addresses to the subsequent 10 addresses\n        txs = []\n        for address in addresses[10:20]:\n            txs.append(await Transaction.create(\n                [],\n                [Output.pay_pubkey_hash(\n                    coins_to_satoshis('1.0'), self.ledger.address_to_hash160(address)\n                )],\n                [self.account], self.account\n            ))\n        await asyncio.wait([self.broadcast(tx) for tx in txs])\n        await asyncio.wait([self.ledger.wait(tx) for tx in txs])\n\n        # verify that a previous bug which failed to save TXIs doesn't come back\n        # this check must happen before generating a new block\n        self.assertTrue(all([\n            tx.inputs[0].txo_ref.txo is not None\n            for tx in await self.ledger.db.get_transactions(txid__in=[tx.id for tx in txs])\n        ]))\n\n        await self.generate(1)\n        await asyncio.wait([self.ledger.wait(tx) for tx in txs])\n        await self.assertBalance(self.account, '199.99876')\n\n        # 10 of the UTXOs have been split into a 1 coin UTXO and a 9 UTXO change\n        self.assertEqual(30, await self.account.get_utxo_count())\n\n        # spend all 30 UTXOs into a a 199 coin UTXO and change\n        tx = await Transaction.create(\n            [],\n            [Output.pay_pubkey_hash(\n                coins_to_satoshis('199.0'), self.ledger.address_to_hash160(addresses[-1])\n            )],\n            [self.account], self.account\n        )\n        await self.broadcast(tx)\n        await self.ledger.wait(tx)\n        await self.generate(1)\n        await self.ledger.wait(tx)\n\n        self.assertEqual(2, await self.account.get_utxo_count())  # 199 + change\n        await self.assertBalance(self.account, '199.99649')\n\n    async def test_sending_and_receiving(self):\n        account1, account2 = self.account, self.wallet.generate_account(self.ledger)\n        await self.ledger.subscribe_account(account2)\n\n        await self.assertBalance(account1, '0.0')\n        await self.assertBalance(account2, '0.0')\n\n        addresses = await account1.receiving.get_addresses()\n        txids = []\n        for address in addresses[:5]:\n            txids.append(await self.send_to_address_and_wait(address, 1.1))\n        await self.generate_and_wait(1, txids)\n        await self.assertBalance(account1, '5.5')\n        await self.assertBalance(account2, '0.0')\n\n        address2 = await account2.receiving.get_or_create_usable_address()\n        tx = await Transaction.create(\n            [],\n            [Output.pay_pubkey_hash(\n                coins_to_satoshis('2.0'), self.ledger.address_to_hash160(address2)\n            )],\n            [account1], account1\n        )\n        await self.broadcast(tx)\n        await self.ledger.wait(tx)  # mempool\n        await self.generate(1)\n        await self.ledger.wait(tx)  # confirmed\n\n        await self.assertBalance(account1, '3.499802')\n        await self.assertBalance(account2, '2.0')\n\n        utxos = await self.account.get_utxos()\n        tx = await Transaction.create(\n            [Input.spend(utxos[0])],\n            [],\n            [account1], account1\n        )\n        await self.broadcast(tx)\n        await self.ledger.wait(tx)  # mempool\n        await self.generate(1)\n        await self.ledger.wait(tx)  # confirmed\n\n        tx = (await account1.get_transactions(include_is_my_input=True, include_is_my_output=True))[1]\n        self.assertEqual(satoshis_to_coins(tx.inputs[0].amount), '1.1')\n        self.assertEqual(satoshis_to_coins(tx.inputs[1].amount), '1.1')\n        self.assertEqual(satoshis_to_coins(tx.outputs[0].amount), '2.0')\n        self.assertEqual(tx.outputs[0].get_address(self.ledger), address2)\n        self.assertTrue(tx.outputs[0].is_internal_transfer)\n        self.assertTrue(tx.outputs[1].is_internal_transfer)\n\n    async def test_history_edge_cases(self):\n        await self.generate(300)\n        await self.assertBalance(self.account, '0.0')\n        address = await self.account.receiving.get_or_create_usable_address()\n        # evil trick: mempool is unsorted on real life, but same order between python instances. reproduce it\n        original_summary = self.conductor.spv_node.server.mempool.transaction_summaries\n\n        def random_summary(*args, **kwargs):\n            summary = original_summary(*args, **kwargs)\n            if summary and len(summary) > 2:\n                ordered = summary.copy()\n                while summary == ordered:\n                    random.shuffle(summary)\n            return summary\n        self.conductor.spv_node.server.mempool.transaction_summaries = random_summary\n        # 10 unconfirmed txs, all from blockchain wallet\n        for i in range(10):\n            await self.send_to_address_and_wait(address, 10)\n        remote_status = await self.ledger.network.subscribe_address(address)\n        self.assertTrue(await self.ledger.update_history(address, remote_status))\n        # 20 unconfirmed txs, 10 from blockchain, 10 from local to local\n        utxos = await self.account.get_utxos()\n        txs = []\n        for utxo in utxos:\n            tx = await Transaction.create(\n                [Input.spend(utxo)],\n                [],\n                [self.account], self.account\n            )\n            await self.broadcast(tx)\n            txs.append(tx)\n        await asyncio.wait([self.on_transaction_address(tx, address) for tx in txs], timeout=1)\n        remote_status = await self.ledger.network.subscribe_address(address)\n        self.assertTrue(await self.ledger.update_history(address, remote_status))\n        # server history grows unordered\n        await self.send_to_address_and_wait(address, 1)\n        self.assertTrue(await self.ledger.update_history(address, remote_status))\n        self.assertEqual(21, len((await self.ledger.get_local_status_and_history(address))[1]))\n        self.assertEqual(0, len(self.ledger._known_addresses_out_of_sync))\n\n    async def _test_transaction(self, send_amount, address, inputs, change):\n        tx = await Transaction.create(\n            [], [Output.pay_pubkey_hash(send_amount, self.ledger.address_to_hash160(address))], [self.account],\n            self.account\n        )\n        await self.ledger.broadcast(tx)\n        input_amounts = [txi.amount for txi in tx.inputs]\n        self.assertListEqual(inputs, input_amounts)\n        self.assertEqual(len(inputs), len(tx.inputs))\n        self.assertEqual(2, len(tx.outputs))\n        self.assertEqual(send_amount, tx.outputs[0].amount)\n        self.assertEqual(change, tx.outputs[1].amount)\n        return tx\n\n    async def assertSpendable(self, amounts):\n        spendable = await self.ledger.db.get_spendable_utxos(\n                self.ledger, 2000000000000, [self.account], set_reserved=False, return_insufficient_funds=True\n            )\n        got_amounts = [estimator.effective_amount for estimator in spendable]\n        self.assertListEqual(sorted(amounts), sorted(got_amounts))\n\n    async def test_sqlite_coin_chooser(self):\n        wallet_manager = WalletManager([self.wallet], {self.ledger.get_id(): self.ledger})\n        await self.generate(300)\n\n        await self.assertBalance(self.account, '0.0')\n        address = await self.account.receiving.get_or_create_usable_address()\n        other_account = self.wallet.generate_account(self.ledger)\n        other_address = await other_account.receiving.get_or_create_usable_address()\n        self.ledger.coin_selection_strategy = 'sqlite'\n        await self.ledger.subscribe_account(other_account)\n\n        accepted = asyncio.ensure_future(self.on_address_update(address))\n        _ = await self.send_to_address_and_wait(address, 1.0)\n        await accepted\n\n        accepted = asyncio.ensure_future(self.on_address_update(address))\n        _ = await self.send_to_address_and_wait(address, 1.0)\n        await accepted\n\n        accepted = asyncio.ensure_future(self.on_address_update(address))\n        _ = await self.send_to_address_and_wait(address, 3.0)\n        await accepted\n\n        accepted = asyncio.ensure_future(self.on_address_update(address))\n        _ = await self.send_to_address_and_wait(address, 5.0)\n        await accepted\n\n        accepted = asyncio.ensure_future(self.on_address_update(address))\n        _ = await self.send_to_address_and_wait(address, 10.0)\n        await accepted\n\n        await self.assertBalance(self.account, '20.0')\n        await self.assertSpendable([99992600, 99992600, 299992600, 499992600, 999992600])\n\n        # send 1.5 lbc\n\n        first_tx = await Transaction.create(\n            [], [Output.pay_pubkey_hash(150000000, self.ledger.address_to_hash160(other_address))], [self.account],\n            self.account\n        )\n\n        self.assertEqual(2, len(first_tx.inputs))\n        self.assertEqual(2, len(first_tx.outputs))\n        self.assertEqual(100000000, first_tx.inputs[0].amount)\n        self.assertEqual(100000000, first_tx.inputs[1].amount)\n        self.assertEqual(150000000, first_tx.outputs[0].amount)\n        self.assertEqual(49980200, first_tx.outputs[1].amount)\n\n        await self.assertBalance(self.account, '18.0')\n        await self.assertSpendable([299992600, 499992600, 999992600])\n\n        await wallet_manager.broadcast_or_release(first_tx, blocking=True)\n        await self.assertSpendable([49972800, 299992600, 499992600, 999992600])\n        # 0.499, 3.0, 5.0, 10.0\n        await self.assertBalance(self.account, '18.499802')\n\n        # send 1.5lbc again\n\n        second_tx = await self._test_transaction(150000000, other_address, [49980200, 300000000], 199960400)\n        await self.assertSpendable([499992600, 999992600])\n\n        # replicate cancelling the api call after the tx broadcast while ledger.wait'ing it\n        e = asyncio.Event()\n\n        real_broadcast = self.ledger.broadcast\n\n        async def broadcast(tx):\n            try:\n                return await real_broadcast(tx)\n            except lbry.wallet.rpc.jsonrpc.RPCError as err:\n                # this is expected in tests where we try to double spend.\n                if 'the transaction was rejected by network rules.' in str(err):\n                    pass\n                else:\n                    raise err\n            finally:\n                e.set()\n\n        self.ledger.broadcast = broadcast\n\n        broadcast_task = asyncio.create_task(wallet_manager.broadcast_or_release(second_tx, blocking=True))\n        # wait for the broadcast to finish\n        await e.wait()\n        # cancel the api call\n        broadcast_task.cancel()\n        with self.assertRaises(asyncio.CancelledError):\n            await broadcast_task\n\n        # test if sending another 1.5 lbc will try to double spend the inputs from the cancelled tx\n        tx1 = await self._test_transaction(150000000, other_address, [500000000], 349987600)\n        await self.ledger.wait(tx1, timeout=1)\n        # wait for the cancelled transaction too, so that it's in the database\n        # needed to keep everything deterministic\n        await self.ledger.wait(second_tx, timeout=1)\n        await self.assertSpendable([199953000, 349980200, 999992600])\n\n        # spend deep into the mempool and see what else breaks\n        tx2 = await self._test_transaction(150000000, other_address, [199960400], 49948000)\n        await self.assertSpendable([349980200, 999992600])\n        await self.ledger.wait(tx2, timeout=1)\n        await self.assertSpendable([49940600, 349980200, 999992600])\n\n        tx3 = await self._test_transaction(150000000, other_address, [49948000, 349987600], 249915800)\n        await self.assertSpendable([999992600])\n        await self.ledger.wait(tx3, timeout=1)\n        await self.assertSpendable([249908400, 999992600])\n\n        tx4 = await self._test_transaction(150000000, other_address, [249915800], 99903400)\n        await self.assertSpendable([999992600])\n        await self.ledger.wait(tx4, timeout=1)\n        await self.assertBalance(self.account, '10.999034')\n        await self.assertSpendable([99896000, 999992600])\n\n        # spend more\n        tx5 = await self._test_transaction(100000000, other_address, [99903400, 1000000000], 999883600)\n        await self.assertSpendable([])\n        await self.ledger.wait(tx5, timeout=1)\n        await self.assertSpendable([999876200])\n        await self.assertBalance(self.account, '9.998836')\n"
  },
  {
    "path": "tests/test_utils.py",
    "content": "import datetime\nimport time\nimport os\nimport tempfile\nimport shutil\nfrom unittest import mock\nfrom binascii import hexlify\n\n\nDEFAULT_TIMESTAMP = datetime.datetime(2016, 1, 1)\nDEFAULT_ISO_TIME = time.mktime(DEFAULT_TIMESTAMP.timetuple())\n\n\ndef mk_db_and_blob_dir():\n    db_dir = tempfile.mkdtemp()\n    blob_dir = tempfile.mkdtemp()\n    return db_dir, blob_dir\n\n\ndef rm_db_and_blob_dir(db_dir, blob_dir):\n    shutil.rmtree(db_dir, ignore_errors=True)\n    shutil.rmtree(blob_dir, ignore_errors=True)\n\n\ndef random_lbry_hash():\n    return hexlify(os.urandom(48)).decode()\n\n\ndef reset_time(test_case, timestamp=DEFAULT_TIMESTAMP):\n    iso_time = time.mktime(timestamp.timetuple())\n    patcher = mock.patch('time.time')\n    patcher.start().return_value = iso_time\n    test_case.addCleanup(patcher.stop)\n\n    patcher = mock.patch('lbry.utils.now')\n    patcher.start().return_value = timestamp\n    test_case.addCleanup(patcher.stop)\n\n    patcher = mock.patch('lbry.utils.utcnow')\n    patcher.start().return_value = timestamp\n    test_case.addCleanup(patcher.stop)\n\n\ndef is_android():\n    return 'ANDROID_ARGUMENT' in os.environ # detect Android using the Kivy way\n"
  },
  {
    "path": "tests/unit/__init__.py",
    "content": ""
  },
  {
    "path": "tests/unit/analytics/__init__.py",
    "content": ""
  },
  {
    "path": "tests/unit/analytics/test_track.py",
    "content": "import lbry.wallet\nfrom lbry.extras.daemon import analytics\n\nimport unittest\n\n\n@unittest.SkipTest\nclass TrackTest(unittest.TestCase):\n    def test_empty_summarize_is_none(self):\n        track = analytics.Manager(None, 'x', 'y', 'z')\n        _, result = track.summarize_and_reset('a')\n        self.assertIsNone(result)\n\n    def test_can_get_sum_of_metric(self):\n        track = analytics.Manager(None, 'x', 'y', 'z')\n        track.add_observation('b', 1)\n        track.add_observation('b', 2)\n\n        _, result = track.summarize_and_reset('b')\n        self.assertEqual(3, result)\n\n    def test_summarize_resets_metric(self):\n        track = analytics.Manager(None, 'x', 'y', 'z')\n        track.add_observation('metric', 1)\n        track.add_observation('metric', 2)\n\n        track.summarize_and_reset('metric')\n        _, result = track.summarize_and_reset('metric')\n        self.assertIsNone(result)\n"
  },
  {
    "path": "tests/unit/blob/__init__.py",
    "content": ""
  },
  {
    "path": "tests/unit/blob/test_blob_file.py",
    "content": "import asyncio\nimport tempfile\nimport shutil\nimport os\nfrom lbry.testcase import AsyncioTestCase\nfrom lbry.error import InvalidDataError, InvalidBlobHashError\nfrom lbry.conf import Config\nfrom lbry.extras.daemon.storage import SQLiteStorage\nfrom lbry.blob.blob_manager import BlobManager\nfrom lbry.blob.blob_file import BlobFile, BlobBuffer, AbstractBlob\n\n\nclass TestBlob(AsyncioTestCase):\n    blob_hash = \"7f5ab2def99f0ddd008da71db3a3772135f4002b19b7605840ed1034c8955431bd7079549e65e6b2a3b9c17c773073ed\"\n    blob_bytes = b'1' * ((2 * 2 ** 20) - 1)\n\n    async def asyncSetUp(self):\n        self.tmp_dir = tempfile.mkdtemp()\n        self.addCleanup(lambda: shutil.rmtree(self.tmp_dir))\n        self.loop = asyncio.get_running_loop()\n        self.config = Config()\n        self.storage = SQLiteStorage(self.config, \":memory:\", self.loop)\n        self.blob_manager = BlobManager(self.loop, self.tmp_dir, self.storage, self.config)\n        await self.storage.open()\n\n    def _get_blob(self, blob_class=AbstractBlob, blob_directory=None):\n        blob = blob_class(self.loop, self.blob_hash, len(self.blob_bytes), self.blob_manager.blob_completed,\n                          blob_directory=blob_directory)\n        self.assertFalse(blob.get_is_verified())\n        self.addCleanup(blob.close)\n        return blob\n\n    async def _test_create_blob(self, blob_class=AbstractBlob, blob_directory=None):\n        blob = self._get_blob(blob_class, blob_directory)\n        writer = blob.get_blob_writer()\n        writer.write(self.blob_bytes)\n        await blob.verified.wait()\n        self.assertTrue(blob.get_is_verified())\n        await asyncio.sleep(0)  # wait for the db save task\n        return blob\n\n    async def _test_close_writers_on_finished(self, blob_class=AbstractBlob, blob_directory=None):\n        blob = self._get_blob(blob_class, blob_directory=blob_directory)\n        writers = [blob.get_blob_writer('1.2.3.4', port) for port in range(5)]\n        self.assertEqual(5, len(blob.writers))\n\n        # test that writing too much causes the writer to fail with InvalidDataError and to be removed\n        with self.assertRaises(InvalidDataError):\n            writers[1].write(self.blob_bytes * 2)\n            await writers[1].finished\n        await asyncio.sleep(0)\n        self.assertEqual(4, len(blob.writers))\n\n        # write the blob\n        other = writers[2]\n        writers[3].write(self.blob_bytes)\n        await blob.verified.wait()\n\n        self.assertTrue(blob.get_is_verified())\n        self.assertEqual(0, len(blob.writers))\n        with self.assertRaises(IOError):\n            other.write(self.blob_bytes)\n\n    def _test_ioerror_if_length_not_set(self, blob_class=AbstractBlob, blob_directory=None):\n        blob = blob_class(\n            self.loop, self.blob_hash, blob_completed_callback=self.blob_manager.blob_completed,\n            blob_directory=blob_directory\n        )\n        self.addCleanup(blob.close)\n        writer = blob.get_blob_writer()\n        with self.assertRaises(IOError):\n            writer.write(b'')\n\n    async def _test_invalid_blob_bytes(self, blob_class=AbstractBlob, blob_directory=None):\n        blob = blob_class(\n            self.loop, self.blob_hash, len(self.blob_bytes), blob_completed_callback=self.blob_manager.blob_completed,\n            blob_directory=blob_directory\n        )\n        self.addCleanup(blob.close)\n        writer = blob.get_blob_writer()\n        writer.write(self.blob_bytes[:-4] + b'fake')\n        with self.assertRaises(InvalidBlobHashError):\n            await writer.finished\n\n    async def test_add_blob_buffer_to_db(self):\n        blob = await self._test_create_blob(BlobBuffer)\n        db_status = await self.storage.get_blob_status(blob.blob_hash)\n        self.assertEqual(db_status, 'pending')\n\n    async def test_add_blob_file_to_db(self):\n        blob = await self._test_create_blob(BlobFile, self.tmp_dir)\n        db_status = await self.storage.get_blob_status(blob.blob_hash)\n        self.assertEqual(db_status, 'finished')\n\n    async def test_invalid_blob_bytes(self):\n        await self._test_invalid_blob_bytes(BlobBuffer)\n        await self._test_invalid_blob_bytes(BlobFile, self.tmp_dir)\n\n    def test_ioerror_if_length_not_set(self):\n        tmp_dir = tempfile.mkdtemp()\n        self.addCleanup(lambda: shutil.rmtree(tmp_dir))\n        self._test_ioerror_if_length_not_set(BlobBuffer)\n        self._test_ioerror_if_length_not_set(BlobFile, tmp_dir)\n\n    async def test_create_blob_file(self):\n        tmp_dir = tempfile.mkdtemp()\n        self.addCleanup(lambda: shutil.rmtree(tmp_dir))\n        blob = await self._test_create_blob(BlobFile, tmp_dir)\n        self.assertIsInstance(blob, BlobFile)\n        self.assertTrue(os.path.isfile(blob.file_path))\n\n        for _ in range(2):\n            with blob.reader_context() as reader:\n                self.assertEqual(self.blob_bytes, reader.read())\n\n    async def test_create_blob_buffer(self):\n        blob = await self._test_create_blob(BlobBuffer)\n        self.assertIsInstance(blob, BlobBuffer)\n        self.assertIsNotNone(blob._verified_bytes)\n\n        # check we can only read the bytes once, and that the buffer is torn down\n        with blob.reader_context() as reader:\n            self.assertEqual(self.blob_bytes, reader.read())\n        self.assertIsNone(blob._verified_bytes)\n        with self.assertRaises(OSError):\n            with blob.reader_context() as reader:\n                self.assertEqual(self.blob_bytes, reader.read())\n        self.assertIsNone(blob._verified_bytes)\n\n    async def test_close_writers_on_finished(self):\n        tmp_dir = tempfile.mkdtemp()\n        self.addCleanup(lambda: shutil.rmtree(tmp_dir))\n        await self._test_close_writers_on_finished(BlobBuffer)\n        await self._test_close_writers_on_finished(BlobFile, tmp_dir)\n\n    async def test_concurrency_and_premature_closes(self):\n        blob_directory = tempfile.mkdtemp()\n        self.addCleanup(lambda: shutil.rmtree(blob_directory))\n        blob = self._get_blob(BlobBuffer, blob_directory=blob_directory)\n        writer = blob.get_blob_writer('1.1.1.1', 1337)\n        self.assertEqual(1, len(blob.writers))\n        with self.assertRaises(OSError):\n            blob.get_blob_writer('1.1.1.1', 1337)\n        writer.close_handle()\n        self.assertTrue(blob.writers[('1.1.1.1', 1337)].closed())\n        writer = blob.get_blob_writer('1.1.1.1', 1337)\n        self.assertEqual(blob.writers[('1.1.1.1', 1337)], writer)\n        writer.close_handle()\n        await asyncio.sleep(0.000000001)  # flush callbacks\n        self.assertEqual(0, len(blob.writers))\n\n    async def test_delete(self):\n        blob_buffer = await self._test_create_blob(BlobBuffer)\n        self.assertIsInstance(blob_buffer, BlobBuffer)\n        self.assertIsNotNone(blob_buffer._verified_bytes)\n        self.assertTrue(blob_buffer.get_is_verified())\n        blob_buffer.delete()\n        self.assertIsNone(blob_buffer._verified_bytes)\n        self.assertFalse(blob_buffer.get_is_verified())\n\n        tmp_dir = tempfile.mkdtemp()\n        self.addCleanup(lambda: shutil.rmtree(tmp_dir))\n\n        blob_file = await self._test_create_blob(BlobFile, tmp_dir)\n        self.assertIsInstance(blob_file, BlobFile)\n        self.assertTrue(os.path.isfile(blob_file.file_path))\n        self.assertTrue(blob_file.get_is_verified())\n        blob_file.delete()\n        self.assertFalse(os.path.isfile(blob_file.file_path))\n        self.assertFalse(blob_file.get_is_verified())\n\n    async def test_delete_corrupt(self):\n        tmp_dir = tempfile.mkdtemp()\n        self.addCleanup(lambda: shutil.rmtree(tmp_dir))\n        blob = BlobFile(\n            self.loop, self.blob_hash, len(self.blob_bytes), blob_completed_callback=self.blob_manager.blob_completed,\n            blob_directory=tmp_dir\n        )\n        writer = blob.get_blob_writer()\n        writer.write(self.blob_bytes)\n        await blob.verified.wait()\n        blob.close()\n        blob = BlobFile(\n            self.loop, self.blob_hash, len(self.blob_bytes), blob_completed_callback=self.blob_manager.blob_completed,\n            blob_directory=tmp_dir\n        )\n        self.assertTrue(blob.get_is_verified())\n\n        with open(blob.file_path, 'wb+') as f:\n            f.write(b'\\x00')\n        blob = BlobFile(\n            self.loop, self.blob_hash, len(self.blob_bytes), blob_completed_callback=self.blob_manager.blob_completed,\n            blob_directory=tmp_dir\n        )\n        self.assertFalse(blob.get_is_verified())\n        self.assertFalse(os.path.isfile(blob.file_path))\n\n    def test_invalid_blob_hash(self):\n        self.assertRaises(InvalidBlobHashError, BlobBuffer, self.loop, '', len(self.blob_bytes))\n        self.assertRaises(InvalidBlobHashError, BlobBuffer, self.loop, 'x' * 96, len(self.blob_bytes))\n        self.assertRaises(InvalidBlobHashError, BlobBuffer, self.loop, 'a' * 97, len(self.blob_bytes))\n\n    async def _test_close_reader(self, blob_class=AbstractBlob, blob_directory=None):\n        blob = await self._test_create_blob(blob_class, blob_directory)\n        reader = blob.reader_context()\n        self.assertEqual(0, len(blob.readers))\n\n        async def read_blob_buffer():\n            with reader as read_handle:\n                self.assertEqual(1, len(blob.readers))\n                await asyncio.sleep(2)\n                self.assertEqual(0, len(blob.readers))\n                return read_handle.read()\n\n        self.loop.call_later(1, blob.close)\n        with self.assertRaises(ValueError) as err:\n            read_task = self.loop.create_task(read_blob_buffer())\n            await read_task\n            self.assertEqual(err.exception, ValueError(\"I/O operation on closed file\"))\n\n    async def test_close_reader(self):\n        tmp_dir = tempfile.mkdtemp()\n        self.addCleanup(lambda: shutil.rmtree(tmp_dir))\n        await self._test_close_reader(BlobBuffer)\n        await self._test_close_reader(BlobFile, tmp_dir)\n"
  },
  {
    "path": "tests/unit/blob/test_blob_manager.py",
    "content": "import tempfile\nimport shutil\nimport os\nfrom lbry.testcase import AsyncioTestCase\nfrom lbry.conf import Config\nfrom lbry.extras.daemon.storage import SQLiteStorage\nfrom lbry.blob.blob_manager import BlobManager\n\n\nclass TestBlobManager(AsyncioTestCase):\n    async def setup_blob_manager(self, save_blobs=True):\n        tmp_dir = tempfile.mkdtemp()\n        self.addCleanup(lambda: shutil.rmtree(tmp_dir))\n        self.config = Config(save_blobs=save_blobs)\n        self.storage = SQLiteStorage(self.config, os.path.join(tmp_dir, \"lbrynet.sqlite\"))\n        self.blob_manager = BlobManager(self.loop, tmp_dir, self.storage, self.config)\n        await self.storage.open()\n\n    async def test_memory_blobs_arent_verified_but_real_ones_are(self):\n        for save_blobs in (False, True):\n            await self.setup_blob_manager(save_blobs=save_blobs)\n            # add a blob file\n            blob_hash = \"7f5ab2def99f0ddd008da71db3a3772135f4002b19b7605840ed1034c8955431bd7079549e65e6b2a3b9c17c773073ed\"\n            blob_bytes = b'1' * ((2 * 2 ** 20) - 1)\n            blob = self.blob_manager.get_blob(blob_hash, len(blob_bytes))\n            blob.save_verified_blob(blob_bytes)\n            await blob.verified.wait()\n            self.assertTrue(blob.get_is_verified())\n            self.blob_manager.blob_completed(blob)\n            self.assertEqual(self.blob_manager.is_blob_verified(blob_hash), save_blobs)\n\n    async def test_sync_blob_file_manager_on_startup(self):\n        await self.setup_blob_manager(save_blobs=True)\n\n        # add a blob file\n        blob_hash = \"7f5ab2def99f0ddd008da71db3a3772135f4002b19b7605840ed1034c8955431bd7079549e65e6b2a3b9c17c773073ed\"\n        blob_bytes = b'1' * ((2 * 2 ** 20) - 1)\n        with open(os.path.join(self.blob_manager.blob_dir, blob_hash), 'wb') as f:\n            f.write(blob_bytes)\n\n        # it should not have been added automatically on startup\n\n        await self.blob_manager.setup()\n        self.assertSetEqual(self.blob_manager.completed_blob_hashes, set())\n\n        # make sure we can add the blob\n        await self.blob_manager.blob_completed(self.blob_manager.get_blob(blob_hash, len(blob_bytes)))\n        self.assertSetEqual(self.blob_manager.completed_blob_hashes, {blob_hash})\n\n        # stop the blob manager and restart it, make sure the blob is there\n        self.blob_manager.stop()\n        self.assertSetEqual(self.blob_manager.completed_blob_hashes, set())\n        await self.blob_manager.setup()\n        self.assertSetEqual(self.blob_manager.completed_blob_hashes, {blob_hash})\n\n        # test that the blob is removed upon the next startup after the file being manually deleted\n        self.blob_manager.stop()\n\n        # manually delete the blob file and restart the blob manager\n        os.remove(os.path.join(self.blob_manager.blob_dir, blob_hash))\n        await self.blob_manager.setup()\n        self.assertSetEqual(self.blob_manager.completed_blob_hashes, set())\n\n        # check that the deleted blob was updated in the database\n        self.assertEqual(\n            'pending', (\n                await self.storage.run_and_return_one_or_none('select status from blob where blob_hash=?', blob_hash)\n            )\n        )\n"
  },
  {
    "path": "tests/unit/blob_exchange/__init__.py",
    "content": ""
  },
  {
    "path": "tests/unit/blob_exchange/test_transfer_blob.py",
    "content": "import asyncio\nimport tempfile\nfrom io import BytesIO\nfrom unittest import mock\n\nimport shutil\nimport os\nimport copy\n\nfrom lbry.blob_exchange.serialization import BlobRequest\nfrom lbry.testcase import AsyncioTestCase\nfrom lbry.conf import Config\nfrom lbry.extras.daemon.storage import SQLiteStorage\nfrom lbry.extras.daemon.daemon import Daemon\nfrom lbry.blob.blob_manager import BlobManager\nfrom lbry.blob_exchange.server import BlobServer, BlobServerProtocol\nfrom lbry.blob_exchange.client import request_blob\nfrom lbry.dht.peer import PeerManager, make_kademlia_peer\nfrom lbry.dht.node import Node\n\n# import logging\n# logging.getLogger(\"lbry\").setLevel(logging.DEBUG)\n\n\ndef mock_config():\n    config = Config(save_files=True)\n    config.fixed_peer_delay = 10000\n    return config\n\n\nclass BlobExchangeTestBase(AsyncioTestCase):\n    async def asyncSetUp(self):\n        self.loop = asyncio.get_event_loop()\n        self.client_wallet_dir = tempfile.mkdtemp()\n        self.client_dir = tempfile.mkdtemp()\n        self.server_dir = tempfile.mkdtemp()\n        self.addCleanup(shutil.rmtree, self.client_wallet_dir)\n        self.addCleanup(shutil.rmtree, self.client_dir)\n        self.addCleanup(shutil.rmtree, self.server_dir)\n        self.server_config = Config(\n            data_dir=self.server_dir,\n            download_dir=self.server_dir,\n            wallet=self.server_dir,\n            save_files=True,\n            fixed_peers=[]\n        )\n        self.server_config.transaction_cache_size = 10000\n        self.server_storage = SQLiteStorage(self.server_config, os.path.join(self.server_dir, \"lbrynet.sqlite\"))\n        self.server_blob_manager = BlobManager(self.loop, self.server_dir, self.server_storage, self.server_config)\n        self.server = BlobServer(self.loop, self.server_blob_manager, 'bQEaw42GXsgCAGio1nxFncJSyRmnztSCjP')\n\n        self.client_config = Config(\n            data_dir=self.client_dir,\n            download_dir=self.client_dir,\n            wallet=self.client_wallet_dir,\n            save_files=True,\n            fixed_peers=[],\n            tracker_servers=[]\n        )\n        self.client_config.transaction_cache_size = 10000\n        self.client_storage = SQLiteStorage(self.client_config, os.path.join(self.client_dir, \"lbrynet.sqlite\"))\n        self.client_blob_manager = BlobManager(self.loop, self.client_dir, self.client_storage, self.client_config)\n        self.client_peer_manager = PeerManager(self.loop)\n        self.server_from_client = make_kademlia_peer(b'1' * 48, \"127.0.0.1\", tcp_port=33333, allow_localhost=True)\n\n        await self.client_storage.open()\n        await self.server_storage.open()\n        await self.client_blob_manager.setup()\n        await self.server_blob_manager.setup()\n\n        self.server.start_server(33333, '127.0.0.1')\n        self.addCleanup(self.server.stop_server)\n        await self.server.started_listening.wait()\n\n\nclass TestBlobExchange(BlobExchangeTestBase):\n    async def _add_blob_to_server(self, blob_hash: str, blob_bytes: bytes):\n        # add the blob on the server\n        server_blob = self.server_blob_manager.get_blob(blob_hash, len(blob_bytes))\n        writer = server_blob.get_blob_writer()\n        writer.write(blob_bytes)\n        await server_blob.verified.wait()\n        self.assertTrue(os.path.isfile(server_blob.file_path))\n        self.assertTrue(server_blob.get_is_verified())\n        self.assertTrue(writer.closed())\n\n    async def _test_transfer_blob(self, blob_hash: str):\n        client_blob = self.client_blob_manager.get_blob(blob_hash)\n\n        # download the blob\n        downloaded, transport = await request_blob(self.loop, client_blob, self.server_from_client.address,\n                                                   self.server_from_client.tcp_port, 2, 3)\n        self.assertIsNotNone(transport)\n        self.addCleanup(transport.close)\n        await client_blob.verified.wait()\n        self.assertTrue(client_blob.get_is_verified())\n        self.assertTrue(downloaded)\n        client_blob.close()\n\n    async def test_transfer_sd_blob(self):\n        sd_hash = \"3e2706157a59aaa47ef52bc264fce488078b4026c0b9bab649a8f2fe1ecc5e5cad7182a2bb7722460f856831a1ac0f02\"\n        mock_sd_blob_bytes = b\"\"\"{\"blobs\": [{\"blob_hash\": \"6f53c72de100f6f007aa1b9720632e2d049cc6049e609ad790b556dba262159f739d5a14648d5701afc84b991254206a\", \"blob_num\": 0, \"iv\": \"3b6110c2d8e742bff66e4314863dee7e\", \"length\": 2097152}, {\"blob_hash\": \"18493bc7c5164b00596153859a0faffa45765e47a6c3f12198a4f7be4658111505b7f8a15ed0162306a0672c4a9b505d\", \"blob_num\": 1, \"iv\": \"df973fa64e73b4ff2677d682cdc32d3e\", \"length\": 2097152}, {\"blob_num\": 2, \"iv\": \"660d2dc2645da7c7d4540a466fcb0c60\", \"length\": 0}], \"key\": \"6465616462656566646561646265656664656164626565666465616462656566\", \"stream_hash\": \"22423c6786584974bd6b462af47ecb03e471da0ef372fe85a4e71a78bef7560c4afb0835c689f03916105404653b7bdf\", \"stream_name\": \"746573745f66696c65\", \"stream_type\": \"lbryfile\", \"suggested_file_name\": \"746573745f66696c65\"}\"\"\"\n        await self._add_blob_to_server(sd_hash, mock_sd_blob_bytes)\n        return await self._test_transfer_blob(sd_hash)\n\n    async def test_transfer_blob(self):\n        blob_hash = \"7f5ab2def99f0ddd008da71db3a3772135f4002b19b7605840ed1034c8955431bd7079549e65e6b2a3b9c17c773073ed\"\n        mock_blob_bytes = b'1' * ((2 * 2 ** 20) - 1)\n        await self._add_blob_to_server(blob_hash, mock_blob_bytes)\n        return await self._test_transfer_blob(blob_hash)\n\n    async def test_host_same_blob_to_multiple_peers_at_once(self):\n        blob_hash = \"7f5ab2def99f0ddd008da71db3a3772135f4002b19b7605840ed1034c8955431bd7079549e65e6b2a3b9c17c773073ed\"\n        mock_blob_bytes = b'1' * ((2 * 2 ** 20) - 1)\n\n        second_client_dir = tempfile.mkdtemp()\n        self.addCleanup(shutil.rmtree, second_client_dir)\n        second_client_conf = Config(save_files=True)\n        second_client_storage = SQLiteStorage(second_client_conf, os.path.join(second_client_dir, \"lbrynet.sqlite\"))\n        second_client_blob_manager = BlobManager(\n            self.loop, second_client_dir, second_client_storage, second_client_conf\n        )\n        server_from_second_client = make_kademlia_peer(b'1' * 48, \"127.0.0.1\", tcp_port=33333, allow_localhost=True)\n\n        await second_client_storage.open()\n        await second_client_blob_manager.setup()\n\n        await self._add_blob_to_server(blob_hash, mock_blob_bytes)\n\n        second_client_blob = second_client_blob_manager.get_blob(blob_hash)\n\n        # download the blob\n        await asyncio.gather(\n            request_blob(\n                self.loop, second_client_blob, server_from_second_client.address,\n                server_from_second_client.tcp_port, 2, 3\n            ),\n            self._test_transfer_blob(blob_hash)\n        )\n        await second_client_blob.verified.wait()\n        self.assertTrue(second_client_blob.get_is_verified())\n\n    async def test_blob_writers_concurrency(self):\n        blob_hash = \"7f5ab2def99f0ddd008da71db3a3772135f4002b19b7605840ed1034c8955431bd7079549e65e6b2a3b9c17c773073ed\"\n        mock_blob_bytes = b'1' * ((2 * 2 ** 20) - 1)\n        blob = self.server_blob_manager.get_blob(blob_hash)\n        write_blob = blob._write_blob\n        write_called_count = 0\n\n        async def _wrap_write_blob(blob_bytes):\n            nonlocal write_called_count\n            write_called_count += 1\n            await write_blob(blob_bytes)\n\n        def wrap_write_blob(blob_bytes):\n            return asyncio.create_task(_wrap_write_blob(blob_bytes))\n\n        blob._write_blob = wrap_write_blob\n\n        writer1 = blob.get_blob_writer(peer_port=1)\n        writer2 = blob.get_blob_writer(peer_port=2)\n        reader1_ctx_before_write = blob.reader_context()\n\n        with self.assertRaises(OSError):\n            blob.get_blob_writer(peer_port=2)\n        with self.assertRaises(OSError):\n            with blob.reader_context():\n                pass\n\n        blob.set_length(len(mock_blob_bytes))\n        results = {}\n\n        def check_finished_callback(writer, num):\n            def inner(writer_future: asyncio.Future):\n                results[num] = writer_future.result()\n            writer.finished.add_done_callback(inner)\n\n        check_finished_callback(writer1, 1)\n        check_finished_callback(writer2, 2)\n\n        def write_task(writer):\n            async def _inner():\n                writer.write(mock_blob_bytes)\n            return self.loop.create_task(_inner())\n\n        await asyncio.gather(write_task(writer1), write_task(writer2))\n\n        self.assertDictEqual({1: mock_blob_bytes, 2: mock_blob_bytes}, results)\n        self.assertEqual(1, write_called_count)\n        await blob.verified.wait()\n        self.assertTrue(blob.get_is_verified())\n        self.assertDictEqual({}, blob.writers)\n\n        with reader1_ctx_before_write as f:\n            self.assertEqual(mock_blob_bytes, f.read())\n        with blob.reader_context() as f:\n            self.assertEqual(mock_blob_bytes, f.read())\n        with blob.reader_context() as f:\n            blob.close()\n            with self.assertRaises(ValueError):\n                f.read()\n        self.assertListEqual([], blob.readers)\n\n    async def test_host_different_blobs_to_multiple_peers_at_once(self):\n        blob_hash = \"7f5ab2def99f0ddd008da71db3a3772135f4002b19b7605840ed1034c8955431bd7079549e65e6b2a3b9c17c773073ed\"\n        mock_blob_bytes = b'1' * ((2 * 2 ** 20) - 1)\n\n        sd_hash = \"3e2706157a59aaa47ef52bc264fce488078b4026c0b9bab649a8f2fe1ecc5e5cad7182a2bb7722460f856831a1ac0f02\"\n        mock_sd_blob_bytes = b\"\"\"{\"blobs\": [{\"blob_hash\": \"6f53c72de100f6f007aa1b9720632e2d049cc6049e609ad790b556dba262159f739d5a14648d5701afc84b991254206a\", \"blob_num\": 0, \"iv\": \"3b6110c2d8e742bff66e4314863dee7e\", \"length\": 2097152}, {\"blob_hash\": \"18493bc7c5164b00596153859a0faffa45765e47a6c3f12198a4f7be4658111505b7f8a15ed0162306a0672c4a9b505d\", \"blob_num\": 1, \"iv\": \"df973fa64e73b4ff2677d682cdc32d3e\", \"length\": 2097152}, {\"blob_num\": 2, \"iv\": \"660d2dc2645da7c7d4540a466fcb0c60\", \"length\": 0}], \"key\": \"6465616462656566646561646265656664656164626565666465616462656566\", \"stream_hash\": \"22423c6786584974bd6b462af47ecb03e471da0ef372fe85a4e71a78bef7560c4afb0835c689f03916105404653b7bdf\", \"stream_name\": \"746573745f66696c65\", \"stream_type\": \"lbryfile\", \"suggested_file_name\": \"746573745f66696c65\"}\"\"\"\n\n        second_client_dir = tempfile.mkdtemp()\n        self.addCleanup(shutil.rmtree, second_client_dir)\n        second_client_conf = Config(save_files=True)\n\n        second_client_storage = SQLiteStorage(second_client_conf, os.path.join(second_client_dir, \"lbrynet.sqlite\"))\n        second_client_blob_manager = BlobManager(\n            self.loop, second_client_dir, second_client_storage, second_client_conf\n        )\n        server_from_second_client = make_kademlia_peer(b'1' * 48, \"127.0.0.1\", tcp_port=33333, allow_localhost=True)\n\n        await second_client_storage.open()\n        await second_client_blob_manager.setup()\n\n        await self._add_blob_to_server(blob_hash, mock_blob_bytes)\n        await self._add_blob_to_server(sd_hash, mock_sd_blob_bytes)\n\n        second_client_blob = self.client_blob_manager.get_blob(blob_hash)\n\n        await asyncio.gather(\n            request_blob(\n                self.loop, second_client_blob, server_from_second_client.address,\n                server_from_second_client.tcp_port, 2, 3\n            ),\n            self._test_transfer_blob(sd_hash),\n            second_client_blob.verified.wait()\n        )\n        self.assertTrue(second_client_blob.get_is_verified())\n\n    async def test_server_chunked_request(self):\n        blob_hash = \"7f5ab2def99f0ddd008da71db3a3772135f4002b19b7605840ed1034c8955431bd7079549e65e6b2a3b9c17c773073ed\"\n        server_protocol = BlobServerProtocol(self.loop, self.server_blob_manager, self.server.lbrycrd_address)\n        transport = mock.Mock(spec=asyncio.Transport)\n        transport.get_extra_info = lambda k: {'peername': ('ip', 90)}[k]\n        received_data = BytesIO()\n        transport.is_closing = lambda: received_data.closed\n        transport.write = received_data.write\n        server_protocol.connection_made(transport)\n        blob_request = BlobRequest.make_request_for_blob_hash(blob_hash).serialize()\n        for byte in blob_request:\n            server_protocol.data_received(bytes([byte]))\n        await asyncio.sleep(0.1)  # yield execution\n        self.assertGreater(len(received_data.getvalue()), 0)\n\n    async def test_idle_timeout(self):\n        self.server.idle_timeout = 1\n\n        blob_hash = \"7f5ab2def99f0ddd008da71db3a3772135f4002b19b7605840ed1034c8955431bd7079549e65e6b2a3b9c17c773073ed\"\n        mock_blob_bytes = b'1' * ((2 * 2 ** 20) - 1)\n        await self._add_blob_to_server(blob_hash, mock_blob_bytes)\n        client_blob = self.client_blob_manager.get_blob(blob_hash)\n\n        # download the blob\n        downloaded, protocol = await request_blob(self.loop, client_blob, self.server_from_client.address,\n                                                   self.server_from_client.tcp_port, 2, 3)\n        self.assertIsNotNone(protocol)\n        self.assertFalse(protocol.transport.is_closing())\n        await client_blob.verified.wait()\n        self.assertTrue(client_blob.get_is_verified())\n        self.assertTrue(downloaded)\n        client_blob.delete()\n\n        # wait for less than the idle timeout\n        await asyncio.sleep(0.5)\n\n        # download the blob again\n        downloaded, protocol2 = await request_blob(self.loop, client_blob, self.server_from_client.address,\n                                                   self.server_from_client.tcp_port, 2, 3,\n                                                    connected_protocol=protocol)\n        self.assertIs(protocol, protocol2)\n        self.assertFalse(protocol.transport.is_closing())\n        await client_blob.verified.wait()\n        self.assertTrue(client_blob.get_is_verified())\n        self.assertTrue(downloaded)\n        client_blob.delete()\n\n        # check that the connection times out from the server side\n        await asyncio.sleep(0.9)\n        self.assertFalse(protocol.transport.is_closing())\n        self.assertIsNotNone(protocol.transport._sock)\n        await asyncio.sleep(0.1)\n        self.assertIsNone(protocol.transport)\n\n    def test_max_request_size(self):\n        protocol = BlobServerProtocol(self.loop, self.server_blob_manager, 'bQEaw42GXsgCAGio1nxFncJSyRmnztSCjP')\n        called = asyncio.Event()\n        protocol.close = called.set\n        protocol.data_received(b'0' * 1199)\n        self.assertFalse(called.is_set())\n        protocol.data_received(b'0')\n        self.assertTrue(called.is_set())\n\n    def test_bad_json(self):\n        protocol = BlobServerProtocol(self.loop, self.server_blob_manager, 'bQEaw42GXsgCAGio1nxFncJSyRmnztSCjP')\n        called = asyncio.Event()\n        protocol.close = called.set\n        protocol.data_received(b'{{0}')\n        self.assertTrue(called.is_set())\n\n    def test_no_request(self):\n        protocol = BlobServerProtocol(self.loop, self.server_blob_manager, 'bQEaw42GXsgCAGio1nxFncJSyRmnztSCjP')\n        called = asyncio.Event()\n        protocol.close = called.set\n        protocol.data_received(b'{}')\n        self.assertTrue(called.is_set())\n\n    async def test_transfer_timeout(self):\n        self.server.transfer_timeout = 1\n\n        blob_hash = \"7f5ab2def99f0ddd008da71db3a3772135f4002b19b7605840ed1034c8955431bd7079549e65e6b2a3b9c17c773073ed\"\n        mock_blob_bytes = b'1' * ((2 * 2 ** 20) - 1)\n        await self._add_blob_to_server(blob_hash, mock_blob_bytes)\n        client_blob = self.client_blob_manager.get_blob(blob_hash)\n        server_blob = self.server_blob_manager.get_blob(blob_hash)\n\n        async def sendfile(writer):\n            await asyncio.sleep(2)\n            return 0\n\n        server_blob.sendfile = sendfile\n\n        with self.assertRaises(asyncio.CancelledError):\n            await request_blob(self.loop, client_blob, self.server_from_client.address,\n                               self.server_from_client.tcp_port, 2, 3)\n\n    async def test_download_blob_using_jsonrpc_blob_get(self):\n        blob_hash = \"7f5ab2def99f0ddd008da71db3a3772135f4002b19b7605840ed1034c8955431bd7079549e65e6b2a3b9c17c773073ed\"\n        mock_blob_bytes = b'1' * ((2 * 2 ** 20) - 1)\n        await self._add_blob_to_server(blob_hash, mock_blob_bytes)\n\n        # setup RPC Daemon\n        daemon_config = copy.deepcopy(self.client_config)\n        daemon_config.fixed_peers = [(self.server_from_client.address, self.server_from_client.tcp_port)]\n        daemon = Daemon(daemon_config)\n\n        mock_node = mock.Mock(spec=Node)\n\n        def _mock_accumulate_peers(q1, q2=None):\n            async def _task():\n                pass\n            q2 = q2 or asyncio.Queue()\n            return q2, self.loop.create_task(_task())\n\n        mock_node.accumulate_peers = _mock_accumulate_peers\n        with mock.patch('lbry.extras.daemon.componentmanager.ComponentManager.all_components_running',\n                        return_value=True):\n            with mock.patch('lbry.extras.daemon.daemon.Daemon.dht_node', new_callable=mock.PropertyMock) \\\n                    as daemon_mock_dht:\n                with mock.patch('lbry.extras.daemon.daemon.Daemon.blob_manager', new_callable=mock.PropertyMock) \\\n                        as daemon_mock_blob_manager:\n                    daemon_mock_dht.return_value = mock_node\n                    daemon_mock_blob_manager.return_value = self.client_blob_manager\n                    result = await daemon.jsonrpc_blob_get(blob_hash, read=True)\n                    self.assertIsNotNone(result)\n                    self.assertEqual(mock_blob_bytes.decode(), result, \"Downloaded blob is different than server blob\")\n"
  },
  {
    "path": "tests/unit/components/__init__.py",
    "content": ""
  },
  {
    "path": "tests/unit/components/test_component_manager.py",
    "content": "import asyncio\nfrom lbry.testcase import AsyncioTestCase, AdvanceTimeTestCase\n\nfrom lbry.conf import Config\nfrom lbry.extras.daemon.componentmanager import ComponentManager\nfrom lbry.extras.daemon.components import DATABASE_COMPONENT, DISK_SPACE_COMPONENT, DHT_COMPONENT, \\\n    BACKGROUND_DOWNLOADER_COMPONENT\nfrom lbry.extras.daemon.components import HASH_ANNOUNCER_COMPONENT, UPNP_COMPONENT\nfrom lbry.extras.daemon.components import PEER_PROTOCOL_SERVER_COMPONENT, EXCHANGE_RATE_MANAGER_COMPONENT\nfrom lbry.extras.daemon import components\n\n\nclass TestComponentManager(AsyncioTestCase):\n\n    def setUp(self):\n        self.default_components_sort = [\n            [\n                components.DatabaseComponent,\n                components.ExchangeRateManagerComponent,\n                components.TorrentComponent,\n                components.UPnPComponent\n            ],\n            [\n                components.BlobComponent,\n                components.DHTComponent,\n                components.WalletComponent\n            ],\n            [\n                components.DiskSpaceComponent,\n                components.FileManagerComponent,\n                components.HashAnnouncerComponent,\n                components.PeerProtocolServerComponent,\n                components.WalletServerPaymentsComponent\n            ],\n            [\n                components.BackgroundDownloaderComponent,\n                components.TrackerAnnouncerComponent\n            ]\n        ]\n        self.component_manager = ComponentManager(Config())\n\n    def test_sort_components(self):\n        stages = self.component_manager.sort_components()\n        for stage_list, sorted_stage_list in zip(stages, self.default_components_sort):\n            self.assertEqual([type(stage) for stage in stage_list], sorted_stage_list)\n\n    def test_sort_components_reverse(self):\n        rev_stages = self.component_manager.sort_components(reverse=True)\n        reverse_default_components_sort = reversed(self.default_components_sort)\n        for stage_list, sorted_stage_list in zip(rev_stages, reverse_default_components_sort):\n            self.assertEqual([type(stage) for stage in stage_list], sorted_stage_list)\n\n    def test_get_component_not_exists(self):\n        with self.assertRaises(NameError):\n            self.component_manager.get_component(\"random_component\")\n\n\nclass TestComponentManagerOverrides(AsyncioTestCase):\n\n    def test_init_with_overrides(self):\n        class FakeWallet:\n            component_name = \"wallet\"\n            depends_on = []\n\n            def __init__(self, component_manager):\n                self.component_manager = component_manager\n\n            @property\n            def component(self):\n                return self\n\n        new_component_manager = ComponentManager(Config(), wallet=FakeWallet)\n        fake_wallet = new_component_manager.get_component(\"wallet\")\n        # wallet should be an instance of FakeWallet and not WalletComponent from components.py\n        self.assertIsInstance(fake_wallet, FakeWallet)\n        self.assertNotIsInstance(fake_wallet, components.WalletComponent)\n\n    def test_init_with_wrong_overrides(self):\n        class FakeRandomComponent:\n            component_name = \"someComponent\"\n            depends_on = []\n\n        with self.assertRaises(SyntaxError):\n            ComponentManager(Config(), randomComponent=FakeRandomComponent)\n\n\nclass FakeComponent:\n    depends_on = []\n    component_name = None\n\n    def __init__(self, component_manager):\n        self.component_manager = component_manager\n        self._running = False\n\n    @property\n    def running(self):\n        return self._running\n\n    async def start(self):\n        pass\n\n    async def stop(self):\n        pass\n\n    @property\n    def component(self):\n        return self\n\n    async def _setup(self):\n        result = await self.start()\n        self._running = True\n        return result\n\n    async def _stop(self):\n        result = await self.stop()\n        self._running = False\n        return result\n\n    async def get_status(self):\n        return {}\n\n    def __lt__(self, other):\n        return self.component_name < other.component_name\n\n\nclass FakeDelayedWallet(FakeComponent):\n    component_name = \"wallet\"\n    depends_on = []\n    ledger = None\n    default_wallet = None\n\n    async def stop(self):\n        await asyncio.sleep(1)\n\n\nclass FakeDelayedBlobManager(FakeComponent):\n    component_name = \"blob_manager\"\n    depends_on = [FakeDelayedWallet.component_name]\n\n    async def start(self):\n        await asyncio.sleep(1)\n\n    async def stop(self):\n        await asyncio.sleep(1)\n\n\nclass FakeDelayedFileManager(FakeComponent):\n    component_name = \"file_manager\"\n    depends_on = [FakeDelayedBlobManager.component_name]\n\n    async def start(self):\n        await asyncio.sleep(1)\n\n    def get_filtered(self):\n        return []\n\n\nclass TestComponentManagerProperStart(AdvanceTimeTestCase):\n\n    def setUp(self):\n        self.component_manager = ComponentManager(\n            Config(),\n            skip_components=[\n                DATABASE_COMPONENT, DISK_SPACE_COMPONENT, DHT_COMPONENT, HASH_ANNOUNCER_COMPONENT,\n                PEER_PROTOCOL_SERVER_COMPONENT, UPNP_COMPONENT, BACKGROUND_DOWNLOADER_COMPONENT,\n                EXCHANGE_RATE_MANAGER_COMPONENT],\n            wallet=FakeDelayedWallet,\n            file_manager=FakeDelayedFileManager,\n            blob_manager=FakeDelayedBlobManager\n        )\n\n    async def test_proper_starting_of_components(self):\n        asyncio.create_task(self.component_manager.start())\n\n        await self.advance(0)\n        self.assertTrue(self.component_manager.get_component('wallet').running)\n        self.assertFalse(self.component_manager.get_component('blob_manager').running)\n        self.assertFalse(self.component_manager.get_component('file_manager').running)\n\n        await self.advance(1)\n        self.assertTrue(self.component_manager.get_component('wallet').running)\n        self.assertTrue(self.component_manager.get_component('blob_manager').running)\n        self.assertFalse(self.component_manager.get_component('file_manager').running)\n\n        await self.advance(1)\n        self.assertTrue(self.component_manager.get_component('wallet').running)\n        self.assertTrue(self.component_manager.get_component('blob_manager').running)\n        self.assertTrue(self.component_manager.get_component('file_manager').running)\n\n    async def test_proper_stopping_of_components(self):\n        asyncio.create_task(self.component_manager.start())\n        await self.advance(0)\n        await self.advance(1)\n        await self.advance(1)\n        self.assertTrue(self.component_manager.get_component('wallet').running)\n        self.assertTrue(self.component_manager.get_component('blob_manager').running)\n        self.assertTrue(self.component_manager.get_component('file_manager').running)\n\n        asyncio.create_task(self.component_manager.stop())\n        await self.advance(0)\n        self.assertFalse(self.component_manager.get_component('file_manager').running)\n        self.assertTrue(self.component_manager.get_component('blob_manager').running)\n        self.assertTrue(self.component_manager.get_component('wallet').running)\n        await self.advance(1)\n        self.assertFalse(self.component_manager.get_component('file_manager').running)\n        self.assertFalse(self.component_manager.get_component('blob_manager').running)\n        self.assertTrue(self.component_manager.get_component('wallet').running)\n        await self.advance(1)\n        self.assertFalse(self.component_manager.get_component('file_manager').running)\n        self.assertFalse(self.component_manager.get_component('blob_manager').running)\n        self.assertFalse(self.component_manager.get_component('wallet').running)\n"
  },
  {
    "path": "tests/unit/core/__init__.py",
    "content": ""
  },
  {
    "path": "tests/unit/core/test_utils.py",
    "content": "import unittest\nimport asyncio\nfrom lbry import utils\nfrom lbry.testcase import AsyncioTestCase\n\n\nclass CompareVersionTest(unittest.TestCase):\n    def test_compare_versions_isnot_lexographic(self):\n        self.assertTrue(utils.version_is_greater_than('0.3.10', '0.3.6'))\n\n    def test_same_versions_return_false(self):\n        self.assertFalse(utils.version_is_greater_than('1.3.9', '1.3.9'))\n\n    def test_same_release_is_greater_then_beta(self):\n        self.assertTrue(utils.version_is_greater_than('1.3.9', '1.3.9b1'))\n\n    def test_version_can_have_four_parts(self):\n        self.assertTrue(utils.version_is_greater_than('1.3.9.1', '1.3.9'))\n\n    def test_release_is_greater_than_rc(self):\n        self.assertTrue(utils.version_is_greater_than('1.3.9', '1.3.9rc0'))\n\n\nclass ObfuscationTest(unittest.TestCase):\n    def test_deobfuscation_reverses_obfuscation(self):\n        plain = \"my_test_string\"\n        obf = utils.obfuscate(plain.encode())\n        self.assertEqual(plain, utils.deobfuscate(obf))\n\n    def test_can_use_unicode(self):\n        plain = '☃'\n        obf = utils.obfuscate(plain.encode())\n        self.assertEqual(plain, utils.deobfuscate(obf))\n\n\nclass SdHashTests(unittest.TestCase):\n\n    def test_none_in_none_out(self):\n        self.assertIsNone(utils.get_sd_hash(None))\n\n    def test_ordinary_dict(self):\n        claim = {\n            \"claim\": {\n                \"value\": {\n                    \"stream\": {\n                        \"source\": {\n                            \"source\": \"0123456789ABCDEF\"\n                        }\n                    }\n                }\n            }\n        }\n        self.assertEqual(\"0123456789ABCDEF\", utils.get_sd_hash(claim))\n\n    def test_old_shape_fails(self):\n        claim = {\n            \"stream\": {\n                \"source\": {\n                    \"source\": \"0123456789ABCDEF\"\n                }\n            }\n        }\n        self.assertIsNone(utils.get_sd_hash(claim))\n\n\nclass CacheConcurrentDecoratorTests(AsyncioTestCase):\n    def setUp(self):\n        self.called = []\n        self.finished = []\n        self.counter = 0\n\n    @utils.cache_concurrent\n    async def foo(self, arg1, arg2=None, delay=1):\n        self.called.append((arg1, arg2, delay))\n        await asyncio.sleep(delay)\n        self.counter += 1\n        self.finished.append((arg1, arg2, delay))\n        return object()\n\n    async def test_gather_duplicates(self):\n        result = await asyncio.gather(\n            self.loop.create_task(self.foo(1)), self.loop.create_task(self.foo(1))\n        )\n        self.assertEqual(1, len(self.called))\n        self.assertEqual(1, len(self.finished))\n        self.assertEqual(1, self.counter)\n        self.assertIs(result[0], result[1])\n        self.assertEqual(2, len(result))\n\n    async def test_one_cancelled_all_cancel(self):\n        t1 = self.loop.create_task(self.foo(1))\n        self.loop.call_later(0.1, t1.cancel)\n\n        with self.assertRaises(asyncio.CancelledError):\n            await asyncio.gather(\n                t1, self.loop.create_task(self.foo(1))\n            )\n        self.assertEqual(1, len(self.called))\n        self.assertEqual(0, len(self.finished))\n        self.assertEqual(0, self.counter)\n\n    async def test_error_after_success(self):\n        def cause_type_error():\n            self.counter = \"\"\n\n        self.loop.call_later(0.1, cause_type_error)\n\n        t1 = self.loop.create_task(self.foo(1))\n        t2 = self.loop.create_task(self.foo(1))\n\n        with self.assertRaises(TypeError):\n            await t2\n        self.assertEqual(1, len(self.called))\n        self.assertEqual(0, len(self.finished))\n        self.assertTrue(t1.done())\n        self.assertEqual(\"\", self.counter)\n\n        # test that the task is run fresh, it should not error\n        self.counter = 0\n        t3 = self.loop.create_task(self.foo(1))\n        self.assertTrue(await t3)\n        self.assertEqual(1, self.counter)\n\n        # the previously failed call should still raise if awaited\n        with self.assertRaises(TypeError):\n            await t1\n\n        self.assertEqual(1, self.counter)\n\n    async def test_break_it(self):\n        t1 = self.loop.create_task(self.foo(1))\n        t2 = self.loop.create_task(self.foo(1))\n        t3 = self.loop.create_task(self.foo(2, delay=0))\n        t3.add_done_callback(lambda _: t2.cancel())\n        with self.assertRaises(asyncio.CancelledError):\n            await asyncio.gather(t1, t2, t3)\n"
  },
  {
    "path": "tests/unit/database/__init__.py",
    "content": ""
  },
  {
    "path": "tests/unit/database/test_SQLiteStorage.py",
    "content": "import shutil\nimport tempfile\nimport unittest\nimport asyncio\nimport logging\nimport hashlib\nfrom lbry.testcase import AsyncioTestCase\nfrom lbry.conf import Config\nfrom lbry.extras.daemon.storage import SQLiteStorage\nfrom lbry.blob.blob_info import BlobInfo\nfrom lbry.blob.blob_manager import BlobManager\nfrom lbry.stream.descriptor import StreamDescriptor\nfrom tests.test_utils import random_lbry_hash\nfrom lbry.dht.peer import make_kademlia_peer\n\nlog = logging.getLogger()\n\n\ndef blob_info_dict(blob_info):\n    info = {\n        \"length\": blob_info.length,\n        \"blob_num\": blob_info.blob_num,\n        \"iv\": blob_info.iv\n    }\n    if blob_info.length:\n        info['blob_hash'] = blob_info.blob_hash\n    return info\n\n\nfake_claim_info = {\n    'name': \"test\",\n    'claim_id': 'deadbeef' * 5,\n    'address': \"bT6wc54qiUUYt34HQF9wnW8b2o2yQTXf2S\",\n    'claim_sequence': 1,\n    'value':  {\n        \"version\": \"_0_0_1\",\n        \"claimType\": \"streamType\",\n        \"stream\": {\n          \"source\": {\n            \"source\": 'deadbeef' * 12,\n            \"version\": \"_0_0_1\",\n            \"contentType\": \"video/mp4\",\n            \"sourceType\": \"lbry_sd_hash\"\n          },\n          \"version\": \"_0_0_1\",\n          \"metadata\": {\n            \"license\": \"LBRY inc\",\n            \"description\": \"What is LBRY? An introduction with Alex Tabarrok\",\n            \"language\": \"en\",\n            \"title\": \"What is LBRY?\",\n            \"author\": \"Samuel Bryan\",\n            \"version\": \"_0_1_0\",\n            \"nsfw\": False,\n            \"licenseUrl\": \"\",\n            \"preview\": \"\",\n            \"thumbnail\": \"https://s3.amazonaws.com/files.lbry.io/logo.png\"\n          }\n        }\n    },\n    'height': 10000,\n    'amount': '1.0',\n    'effective_amount': '1.0',\n    'nout': 0,\n    'txid': \"deadbeef\" * 8,\n    'supports': [],\n    'channel_claim_id': None,\n    'channel_name': None\n}\n\n\nclass StorageTest(AsyncioTestCase):\n    async def asyncSetUp(self):\n        self.conf = Config()\n        self.storage = SQLiteStorage(self.conf, ':memory:')\n        self.blob_dir = tempfile.mkdtemp()\n        self.addCleanup(shutil.rmtree, self.blob_dir)\n        self.blob_manager = BlobManager(asyncio.get_event_loop(), self.blob_dir, self.storage, self.conf)\n        await self.storage.open()\n\n    async def asyncTearDown(self):\n        await self.storage.close()\n\n    async def store_fake_blob(self, blob_hash, length=100):\n        await self.storage.add_blobs((blob_hash, length, 0, 0), finished=True)\n\n    async def store_fake_stream(self, stream_hash, blobs=None, file_name=\"fake_file\", key=\"DEADBEEF\"):\n        blobs = blobs or [BlobInfo(1, 100, \"DEADBEEF\", 0, random_lbry_hash())]\n        descriptor = StreamDescriptor(\n            asyncio.get_event_loop(), self.blob_dir, file_name, key, file_name, blobs, stream_hash\n        )\n        sd_blob = await descriptor.make_sd_blob()\n        await self.storage.store_stream(sd_blob, descriptor)\n        return descriptor\n\n    async def make_and_store_fake_stream(self, blob_count=2, stream_hash=None):\n        stream_hash = stream_hash or random_lbry_hash()\n        blobs = [\n            BlobInfo(i + 1, 100, \"DEADBEEF\", 0, random_lbry_hash())\n            for i in range(blob_count)\n        ]\n        await self.store_fake_stream(stream_hash, blobs)\n\n\nclass TestSQLiteStorage(StorageTest):\n    async def test_setup(self):\n        files = await self.storage.get_all_lbry_files()\n        self.assertEqual(len(files), 0)\n        blobs = await self.storage.get_all_blob_hashes()\n        self.assertEqual(len(blobs), 0)\n\n    async def test_store_blob(self):\n        blob_hash = random_lbry_hash()\n        await self.store_fake_blob(blob_hash)\n        blob_hashes = await self.storage.get_all_blob_hashes()\n        self.assertEqual(blob_hashes, [blob_hash])\n\n    async def test_delete_blob(self):\n        blob_hash = random_lbry_hash()\n        await self.store_fake_blob(blob_hash)\n        blob_hashes = await self.storage.get_all_blob_hashes()\n        self.assertEqual(blob_hashes, [blob_hash])\n        await self.storage.delete_blobs_from_db(blob_hashes)\n        blob_hashes = await self.storage.get_all_blob_hashes()\n        self.assertEqual(blob_hashes, [])\n\n    async def test_supports_storage(self):\n        claim_ids = [random_lbry_hash() for _ in range(10)]\n        random_supports = [{\n            \"txid\": random_lbry_hash(),\n            \"nout\": i,\n            \"address\": f\"addr{i}\",\n            \"amount\": f\"{i}.0\"\n        } for i in range(20)]\n        expected_supports = {}\n        for idx, claim_id in enumerate(claim_ids):\n            await self.storage.save_supports({claim_id: random_supports[idx*2:idx*2+2]})\n            for random_support in random_supports[idx*2:idx*2+2]:\n                random_support['claim_id'] = claim_id\n                expected_supports.setdefault(claim_id, []).append(random_support)\n\n        supports = await self.storage.get_supports(claim_ids[0])\n        self.assertEqual(supports, expected_supports[claim_ids[0]])\n        all_supports = await self.storage.get_supports(*claim_ids)\n        for support in all_supports:\n            self.assertIn(support, expected_supports[support['claim_id']])\n\n\nclass StreamStorageTests(StorageTest):\n    async def test_store_and_delete_stream(self):\n        stream_hash = random_lbry_hash()\n        descriptor = await self.store_fake_stream(stream_hash)\n        files = await self.storage.get_all_lbry_files()\n        self.assertListEqual(files, [])\n        stream_hashes = await self.storage.get_all_stream_hashes()\n        self.assertListEqual(stream_hashes, [stream_hash])\n        await self.storage.delete_stream(descriptor)\n        files = await self.storage.get_all_lbry_files()\n        self.assertListEqual(files, [])\n        stream_hashes = await self.storage.get_all_stream_hashes()\n        self.assertListEqual(stream_hashes, [])\n\n\n@unittest.SkipTest\nclass FileStorageTests(StorageTest):\n    async def test_store_file(self):\n        download_directory = self.db_dir\n        out = await self.storage.get_all_lbry_files()\n        self.assertEqual(len(out), 0)\n\n        stream_hash = random_lbry_hash()\n        sd_hash = random_lbry_hash()\n        blob1 = random_lbry_hash()\n        blob2 = random_lbry_hash()\n\n        await self.store_fake_blob(sd_hash)\n        await self.store_fake_blob(blob1)\n        await self.store_fake_blob(blob2)\n\n        await self.store_fake_stream(stream_hash, sd_hash)\n        await self.store_fake_stream_blob(stream_hash, blob1, 1)\n        await self.store_fake_stream_blob(stream_hash, blob2, 2)\n\n        blob_data_rate = 0\n        file_name = \"test file\"\n        await self.storage.save_published_file(\n            stream_hash, file_name, download_directory, blob_data_rate\n        )\n\n        files = await self.storage.get_all_lbry_files()\n        self.assertEqual(1, len(files))\n\n\n@unittest.SkipTest\nclass ContentClaimStorageTests(StorageTest):\n    async def test_store_content_claim(self):\n        download_directory = self.db_dir\n        out = await self.storage.get_all_lbry_files()\n        self.assertEqual(len(out), 0)\n\n        stream_hash = random_lbry_hash()\n        sd_hash = fake_claim_info['value']['stream']['source']['source']\n\n        # test that we can associate a content claim to a file\n        # use the generated sd hash in the fake claim\n        fake_outpoint = \"%s:%i\" % (fake_claim_info['txid'], fake_claim_info['nout'])\n\n        await self.make_and_store_fake_stream(blob_count=2, stream_hash=stream_hash, sd_hash=sd_hash)\n        blob_data_rate = 0\n        file_name = \"test file\"\n        await self.storage.save_published_file(\n            stream_hash, file_name, download_directory, blob_data_rate\n        )\n        await self.storage.save_claims([fake_claim_info])\n        await self.storage.save_content_claim(stream_hash, fake_outpoint)\n        stored_content_claim = await self.storage.get_content_claim(stream_hash)\n        self.assertDictEqual(stored_content_claim, fake_claim_info)\n\n        stream_hashes = await self.storage.get_old_stream_hashes_for_claim_id(fake_claim_info['claim_id'],\n                                                                              stream_hash)\n        self.assertListEqual(stream_hashes, [])\n\n        # test that we can't associate a claim update with a new stream to the file\n        second_stream_hash, second_sd_hash = random_lbry_hash(), random_lbry_hash()\n        await self.make_and_store_fake_stream(blob_count=2, stream_hash=second_stream_hash, sd_hash=second_sd_hash)\n        with self.assertRaisesRegex(Exception, \"stream mismatch\"):\n            await self.storage.save_content_claim(second_stream_hash, fake_outpoint)\n\n        # test that we can associate a new claim update containing the same stream to the file\n        update_info = deepcopy(fake_claim_info)\n        update_info['txid'] = \"beef0000\" * 12\n        update_info['nout'] = 0\n        second_outpoint = \"%s:%i\" % (update_info['txid'], update_info['nout'])\n        await self.storage.save_claims([update_info])\n        await self.storage.save_content_claim(stream_hash, second_outpoint)\n        update_info_result = await self.storage.get_content_claim(stream_hash)\n        self.assertDictEqual(update_info_result, update_info)\n\n        # test that we can't associate an update with a mismatching claim id\n        invalid_update_info = deepcopy(fake_claim_info)\n        invalid_update_info['txid'] = \"beef0001\" * 12\n        invalid_update_info['nout'] = 0\n        invalid_update_info['claim_id'] = \"beef0002\" * 5\n        invalid_update_outpoint = \"%s:%i\" % (invalid_update_info['txid'], invalid_update_info['nout'])\n        with self.assertRaisesRegex(Exception, \"mismatching claim ids when updating stream \"\n                                               \"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef \"\n                                               \"vs beef0002beef0002beef0002beef0002beef0002\"):\n            await self.storage.save_claims([invalid_update_info])\n            await self.storage.save_content_claim(stream_hash, invalid_update_outpoint)\n        current_claim_info = await self.storage.get_content_claim(stream_hash)\n        # this should still be the previous update\n        self.assertDictEqual(current_claim_info, update_info)\n\n\nclass UpdatePeersTest(StorageTest):\n    async def test_update_get_peers(self):\n        node_id = hashlib.sha384(\"1234\".encode()).digest()\n        args = (node_id, '73.186.148.72', 4444, None)\n        fake_peer = make_kademlia_peer(*args)\n        await self.storage.save_kademlia_peers([fake_peer])\n        peers = await self.storage.get_persisted_kademlia_peers()\n        self.assertTupleEqual(args, peers[0])\n"
  },
  {
    "path": "tests/unit/dht/__init__.py",
    "content": ""
  },
  {
    "path": "tests/unit/dht/protocol/__init__.py",
    "content": ""
  },
  {
    "path": "tests/unit/dht/protocol/test_data_store.py",
    "content": "import asyncio\nfrom unittest import mock, TestCase\nfrom lbry.dht.protocol.data_store import DictDataStore\nfrom lbry.dht.peer import PeerManager, make_kademlia_peer\n\n\nclass DataStoreTests(TestCase):\n    def setUp(self):\n        self.loop = mock.Mock(spec=asyncio.BaseEventLoop)\n        self.loop.time = lambda: 0.0\n        self.peer_manager = PeerManager(self.loop)\n        self.data_store = DictDataStore(self.loop, self.peer_manager)\n\n    def _test_add_peer_to_blob(self, blob=b'2' * 48, node_id=b'1' * 48, address='1.2.3.4', tcp_port=3333,\n                               udp_port=4444):\n        peer = make_kademlia_peer(node_id, address, udp_port)\n        peer.update_tcp_port(tcp_port)\n        before = self.data_store.get_peers_for_blob(blob)\n        self.data_store.add_peer_to_blob(peer, blob)\n        self.assertListEqual(before + [peer], self.data_store.get_peers_for_blob(blob))\n        return peer\n\n    def test_refresh_peer_to_blob(self):\n        blob = b'f' * 48\n        self.assertListEqual([], self.data_store.get_peers_for_blob(blob))\n        peer = self._test_add_peer_to_blob(blob=blob, node_id=b'a' * 48, address='1.2.3.4')\n        self.assertTrue(self.data_store.has_peers_for_blob(blob))\n        self.assertEqual(len(self.data_store.get_peers_for_blob(blob)), 1)\n        self.assertEqual(self.data_store._data_store[blob][0][1], 0)\n        self.loop.time = lambda: 100.0\n        self.assertEqual(self.data_store._data_store[blob][0][1], 0)\n        self.data_store.add_peer_to_blob(peer, blob)\n        self.assertEqual(self.data_store._data_store[blob][0][1], 100)\n\n    def test_add_peer_to_blob(self, blob=b'f' * 48, peers=None):\n        peers = peers or [\n            (b'a' * 48, '1.2.3.4'),\n            (b'b' * 48, '1.2.3.5'),\n            (b'c' * 48, '1.2.3.6'),\n        ]\n        self.assertListEqual([], self.data_store.get_peers_for_blob(blob))\n        peer_objects = []\n        for (node_id, address) in peers:\n            peer_objects.append(self._test_add_peer_to_blob(blob=blob, node_id=node_id, address=address))\n            self.assertTrue(self.data_store.has_peers_for_blob(blob))\n        self.assertEqual(len(self.data_store.get_peers_for_blob(blob)), len(peers))\n        return peer_objects\n\n    def test_get_storing_contacts(self, peers=None, blob1=b'd' * 48, blob2=b'e' * 48):\n        peers = peers or [\n            (b'a' * 48, '1.2.3.4'),\n            (b'b' * 48, '1.2.3.5'),\n            (b'c' * 48, '1.2.3.6'),\n        ]\n        peer_objs1 = self.test_add_peer_to_blob(blob=blob1, peers=peers)\n        self.assertEqual(len(peers), len(peer_objs1))\n        self.assertEqual(len(peers), len(self.data_store.get_storing_contacts()))\n\n        peer_objs2 = self.test_add_peer_to_blob(blob=blob2, peers=peers)\n        self.assertEqual(len(peers), len(peer_objs2))\n        self.assertEqual(len(peers), len(self.data_store.get_storing_contacts()))\n\n        for o1, o2 in zip(peer_objs1, peer_objs2):\n            self.assertIs(o1, o2)\n\n    def test_remove_expired_peers(self):\n        peers = [\n            (b'a' * 48, '1.2.3.4'),\n            (b'b' * 48, '1.2.3.5'),\n            (b'c' * 48, '1.2.3.6'),\n        ]\n        blob1 = b'd' * 48\n        blob2 = b'e' * 48\n\n        self.data_store.removed_expired_peers()  # nothing should happen\n        self.test_get_storing_contacts(peers, blob1, blob2)\n        self.assertEqual(len(self.data_store.get_peers_for_blob(blob1)), len(peers))\n        self.assertEqual(len(self.data_store.get_peers_for_blob(blob2)), len(peers))\n        self.assertEqual(len(self.data_store.get_storing_contacts()), len(peers))\n\n        # expire the first peer from blob1\n        first = self.data_store._data_store[blob1][0][0]\n        self.data_store._data_store[blob1][0] = (first, -86401)\n        self.assertEqual(len(self.data_store.get_storing_contacts()), len(peers))\n        self.data_store.removed_expired_peers()\n        self.assertEqual(len(self.data_store.get_peers_for_blob(blob1)), len(peers) - 1)\n        self.assertEqual(len(self.data_store.get_peers_for_blob(blob2)), len(peers))\n        self.assertEqual(len(self.data_store.get_storing_contacts()), len(peers))\n\n        # expire the first peer from blob2\n        first = self.data_store._data_store[blob2][0][0]\n        self.data_store._data_store[blob2][0] = (first, -86401)\n        self.data_store.removed_expired_peers()\n        self.assertEqual(len(self.data_store.get_peers_for_blob(blob1)), len(peers) - 1)\n        self.assertEqual(len(self.data_store.get_peers_for_blob(blob2)), len(peers) - 1)\n        self.assertEqual(len(self.data_store.get_storing_contacts()), len(peers) - 1)\n\n        # expire the second and third peers from blob1\n        first = self.data_store._data_store[blob2][0][0]\n        self.data_store._data_store[blob1][0] = (first, -86401)\n        second = self.data_store._data_store[blob2][1][0]\n        self.data_store._data_store[blob1][1] = (second, -86401)\n        self.data_store.removed_expired_peers()\n        self.assertEqual(len(self.data_store.get_peers_for_blob(blob1)), 0)\n        self.assertEqual(len(self.data_store.get_peers_for_blob(blob2)), len(peers) - 1)\n        self.assertEqual(len(self.data_store.get_storing_contacts()), len(peers) - 1)\n"
  },
  {
    "path": "tests/unit/dht/protocol/test_distance.py",
    "content": "import unittest\nfrom lbry.dht.protocol.distance import Distance\n\n\nclass DistanceTests(unittest.TestCase):\n    def test_invalid_key_length(self):\n        self.assertRaises(ValueError, Distance, b'1' * 47)\n        self.assertRaises(ValueError, Distance, b'1' * 49)\n        self.assertRaises(ValueError, Distance, b'')\n\n        self.assertRaises(ValueError, Distance(b'0' * 48), b'1' * 47)\n        self.assertRaises(ValueError, Distance(b'0' * 48), b'1' * 49)\n        self.assertRaises(ValueError, Distance(b'0' * 48), b'')\n"
  },
  {
    "path": "tests/unit/dht/protocol/test_kbucket.py",
    "content": "import struct\nimport asyncio\nfrom lbry.utils import generate_id\nfrom lbry.dht.protocol.routing_table import KBucket\nfrom lbry.dht.peer import PeerManager, make_kademlia_peer\nfrom lbry.dht import constants\nfrom lbry.testcase import AsyncioTestCase\n\n\ndef address_generator(address=(1, 2, 3, 4)):\n    def increment(addr):\n        value = struct.unpack(\"I\", \"\".join([chr(x) for x in list(addr)[::-1]]).encode())[0] + 1\n        new_addr = []\n        for i in range(4):\n            new_addr.append(value % 256)\n            value >>= 8\n        return tuple(new_addr[::-1])\n\n    while True:\n        yield \"{}.{}.{}.{}\".format(*address)\n        address = increment(address)\n\n\nclass TestKBucket(AsyncioTestCase):\n    def setUp(self):\n        self.loop = asyncio.get_event_loop()\n        self.address_generator = address_generator()\n        self.peer_manager = PeerManager(self.loop)\n        self.kbucket = KBucket(self.peer_manager, 0, 2 ** constants.HASH_BITS, generate_id())\n\n    def test_add_peer(self):\n        peer = make_kademlia_peer(constants.generate_id(2), \"1.2.3.4\", udp_port=4444)\n        peer_update2 = make_kademlia_peer(constants.generate_id(2), \"1.2.3.4\", udp_port=4445)\n\n        self.assertListEqual([], self.kbucket.peers)\n\n        # add the peer\n        self.kbucket.add_peer(peer)\n        self.assertListEqual([peer], self.kbucket.peers)\n\n        # re-add it\n        self.kbucket.add_peer(peer)\n        self.assertListEqual([peer], self.kbucket.peers)\n        self.assertEqual(self.kbucket.peers[0].udp_port, 4444)\n\n        # add a new peer object with the same id and address but a different port\n        self.kbucket.add_peer(peer_update2)\n        self.assertListEqual([peer_update2], self.kbucket.peers)\n        self.assertEqual(self.kbucket.peers[0].udp_port, 4445)\n\n        # modify the peer object to have a different port\n        peer_update2.udp_port = 4444\n        self.kbucket.add_peer(peer_update2)\n        self.assertListEqual([peer_update2], self.kbucket.peers)\n        self.assertEqual(self.kbucket.peers[0].udp_port, 4444)\n\n        self.kbucket.peers.clear()\n\n        # Test if contacts can be added to empty list\n        # Add k contacts to bucket\n        for i in range(constants.K):\n            peer = make_kademlia_peer(generate_id(), next(self.address_generator), 4444)\n            self.assertTrue(self.kbucket.add_peer(peer))\n            self.assertEqual(peer, self.kbucket.peers[i])\n\n        # Test if contact is not added to full list\n        peer = make_kademlia_peer(generate_id(), next(self.address_generator), 4444)\n        self.assertFalse(self.kbucket.add_peer(peer))\n\n        # Test if an existing contact is updated correctly if added again\n        existing_peer = self.kbucket.peers[0]\n        self.assertTrue(self.kbucket.add_peer(existing_peer))\n        self.assertEqual(existing_peer, self.kbucket.peers[-1])\n\n    # def testGetContacts(self):\n    #     # try and get 2 contacts from empty list\n    #     result = self.kbucket.getContacts(2)\n    #     self.assertFalse(len(result) != 0, \"Returned list should be empty; returned list length: %d\" %\n    #                 (len(result)))\n    #\n    #     # Add k-2 contacts\n    #     node_ids = []\n    #     if constants.k >= 2:\n    #         for i in range(constants.k-2):\n    #             node_ids.append(generate_id())\n    #             tmpContact = self.contact_manager.make_contact(node_ids[-1], next(self.address_generator), 4444, 0,\n    #                                                            None)\n    #             self.kbucket.addContact(tmpContact)\n    #     else:\n    #         # add k contacts\n    #         for i in range(constants.k):\n    #             node_ids.append(generate_id())\n    #             tmpContact = self.contact_manager.make_contact(node_ids[-1], next(self.address_generator), 4444, 0,\n    #                                                            None)\n    #             self.kbucket.addContact(tmpContact)\n    #\n    #     # try to get too many contacts\n    #     # requested count greater than bucket size; should return at most k contacts\n    #     contacts = self.kbucket.getContacts(constants.k+3)\n    #     self.assertTrue(len(contacts) <= constants.k,\n    #                     'Returned list should not have more than k entries!')\n    #\n    #     # verify returned contacts in list\n    #     for node_id, i in zip(node_ids, range(constants.k-2)):\n    #         self.assertFalse(self.kbucket._contacts[i].id != node_id,\n    #                     \"Contact in position %s not same as added contact\" % (str(i)))\n    #\n    #     # try to get too many contacts\n    #     # requested count one greater than number of contacts\n    #     if constants.k >= 2:\n    #         result = self.kbucket.getContacts(constants.k-1)\n    #         self.assertFalse(len(result) != constants.k-2,\n    #                     \"Too many contacts in returned list %s - should be %s\" %\n    #                     (len(result), constants.k-2))\n    #     else:\n    #         result = self.kbucket.getContacts(constants.k-1)\n    #         # if the count is <= 0, it should return all of it's contats\n    #         self.assertFalse(len(result) != constants.k,\n    #                     \"Too many contacts in returned list %s - should be %s\" %\n    #                     (len(result), constants.k-2))\n    #         result = self.kbucket.getContacts(constants.k-3)\n    #         self.assertFalse(len(result) != constants.k-3,\n    #                     \"Too many contacts in returned list %s - should be %s\" %\n    #                     (len(result), constants.k-3))\n\n    def test_remove_peer(self):\n        # try remove contact from empty list\n        peer = make_kademlia_peer(generate_id(), next(self.address_generator), 4444)\n        self.assertRaises(ValueError, self.kbucket.remove_peer, peer)\n\n        added = []\n        # Add couple contacts\n        for i in range(constants.K - 2):\n            peer = make_kademlia_peer(generate_id(), next(self.address_generator), 4444)\n            self.assertTrue(self.kbucket.add_peer(peer))\n            added.append(peer)\n\n        while added:\n            peer = added.pop()\n            self.assertIn(peer, self.kbucket.peers)\n            self.kbucket.remove_peer(peer)\n            self.assertNotIn(peer, self.kbucket.peers)\n"
  },
  {
    "path": "tests/unit/dht/protocol/test_protocol.py",
    "content": "import asyncio\nimport binascii\nfrom lbry.testcase import AsyncioTestCase\nfrom tests import dht_mocks\nfrom lbry.dht.serialization.bencoding import bencode, bdecode\nfrom lbry.dht import constants\nfrom lbry.dht.protocol.protocol import KademliaProtocol\nfrom lbry.dht.peer import PeerManager, make_kademlia_peer\n\n\nclass TestProtocol(AsyncioTestCase):\n    async def test_ping(self):\n        loop = asyncio.get_event_loop()\n        with dht_mocks.mock_network_loop(loop):\n            node_id1 = constants.generate_id()\n            peer1 = KademliaProtocol(\n                loop, PeerManager(loop), node_id1, '1.2.3.4', 4444, 3333\n            )\n            peer2 = KademliaProtocol(\n                loop, PeerManager(loop), constants.generate_id(), '1.2.3.5', 4444, 3333\n            )\n            await loop.create_datagram_endpoint(lambda: peer1, ('1.2.3.4', 4444))\n            await loop.create_datagram_endpoint(lambda: peer2, ('1.2.3.5', 4444))\n\n            peer = make_kademlia_peer(node_id1, '1.2.3.4', udp_port=4444)\n            result = await peer2.get_rpc_peer(peer).ping()\n            self.assertEqual(result, b'pong')\n            peer1.stop()\n            peer2.stop()\n            peer1.disconnect()\n            peer2.disconnect()\n\n    async def test_update_token(self):\n        loop = asyncio.get_event_loop()\n        with dht_mocks.mock_network_loop(loop):\n            node_id1 = constants.generate_id()\n            peer1 = KademliaProtocol(\n                loop, PeerManager(loop), node_id1, '1.2.3.4', 4444, 3333\n            )\n            peer2 = KademliaProtocol(\n                loop, PeerManager(loop), constants.generate_id(), '1.2.3.5', 4444, 3333\n            )\n            await loop.create_datagram_endpoint(lambda: peer1, ('1.2.3.4', 4444))\n            await loop.create_datagram_endpoint(lambda: peer2, ('1.2.3.5', 4444))\n\n            peer = make_kademlia_peer(node_id1, '1.2.3.4', udp_port=4444)\n            self.assertEqual(None, peer2.peer_manager.get_node_token(peer.node_id))\n            await peer2.get_rpc_peer(peer).find_value(b'1' * 48)\n            self.assertNotEqual(None, peer2.peer_manager.get_node_token(peer.node_id))\n            peer1.stop()\n            peer2.stop()\n            peer1.disconnect()\n            peer2.disconnect()\n\n    async def test_store_to_peer(self):\n        loop = asyncio.get_event_loop()\n        with dht_mocks.mock_network_loop(loop):\n            node_id1 = constants.generate_id()\n            peer1 = KademliaProtocol(\n                loop, PeerManager(loop), node_id1, '1.2.3.4', 4444, 3333\n            )\n            peer2 = KademliaProtocol(\n                loop, PeerManager(loop), constants.generate_id(), '1.2.3.5', 4444, 3333\n            )\n            await loop.create_datagram_endpoint(lambda: peer1, ('1.2.3.4', 4444))\n            await loop.create_datagram_endpoint(lambda: peer2, ('1.2.3.5', 4444))\n\n            peer = make_kademlia_peer(node_id1, '1.2.3.4', udp_port=4444)\n            peer2_from_peer1 = make_kademlia_peer(\n                peer2.node_id, peer2.external_ip, udp_port=peer2.udp_port\n            )\n            peer2_from_peer1.update_tcp_port(3333)\n            peer3 = make_kademlia_peer(\n                constants.generate_id(), '1.2.3.6', udp_port=4444\n            )\n            store_result = await peer2.store_to_peer(b'2' * 48, peer)\n            self.assertEqual(store_result[0], peer.node_id)\n            self.assertTrue(store_result[1])\n            self.assertTrue(peer1.data_store.has_peers_for_blob(b'2' * 48))\n            self.assertFalse(peer1.data_store.has_peers_for_blob(b'3' * 48))\n            self.assertListEqual([peer2_from_peer1], peer1.data_store.get_storing_contacts())\n            peer1.data_store.completed_blobs.add(binascii.hexlify(b'2' * 48).decode())\n            find_value_response = peer1.node_rpc.find_value(peer3, b'2' * 48)\n            self.assertEqual(len(find_value_response[b'contacts']), 0)\n            self.assertSetEqual(\n                {b'2' * 48, b'token', b'protocolVersion', b'contacts', b'p'}, set(find_value_response.keys())\n            )\n            self.assertEqual(2, len(find_value_response[b'2' * 48]))\n            self.assertEqual(find_value_response[b'2' * 48][0], peer2_from_peer1.compact_address_tcp())\n            self.assertDictEqual(bdecode(bencode(find_value_response)), find_value_response)\n\n            find_value_page_above_pages_response = peer1.node_rpc.find_value(peer3, b'2' * 48, page=10)\n            self.assertNotIn(b'2' * 48, find_value_page_above_pages_response)\n\n            peer1.stop()\n            peer2.stop()\n            peer1.disconnect()\n            peer2.disconnect()\n\n    async def _make_protocol(self, other_peer, node_id, address, udp_port, tcp_port):\n        proto = KademliaProtocol(\n            self.loop, PeerManager(self.loop), node_id, address, udp_port, tcp_port\n        )\n        await self.loop.create_datagram_endpoint(lambda: proto, (address, 4444))\n        proto.start()\n        return proto, make_kademlia_peer(node_id, address, udp_port=udp_port)\n\n    async def test_add_peer_after_handle_request(self):\n        with dht_mocks.mock_network_loop(self.loop):\n            node_id1 = constants.generate_id()\n            node_id2 = constants.generate_id()\n            node_id3 = constants.generate_id()\n            node_id4 = constants.generate_id()\n\n            peer1 = KademliaProtocol(\n                self.loop, PeerManager(self.loop), node_id1, '1.2.3.4', 4444, 3333\n            )\n            await self.loop.create_datagram_endpoint(lambda: peer1, ('1.2.3.4', 4444))\n            peer1.start()\n\n            peer2, peer_2_from_peer_1 = await self._make_protocol(peer1, node_id2, '1.2.3.5', 4444, 3333)\n            peer3, peer_3_from_peer_1 = await self._make_protocol(peer1, node_id3, '1.2.3.6', 4444, 3333)\n            peer4, peer_4_from_peer_1 = await self._make_protocol(peer1, node_id4, '1.2.3.7', 4444, 3333)\n\n            # peers who reply should be added\n            await peer1.get_rpc_peer(peer_2_from_peer_1).ping()\n            await asyncio.sleep(0.5)\n            self.assertListEqual([peer_2_from_peer_1], peer1.routing_table.get_peers())\n            peer1.routing_table.remove_peer(peer_2_from_peer_1)\n\n            # peers not known by be good/bad should be enqueued to maybe-ping\n            peer1_from_peer3 = peer3.get_rpc_peer(make_kademlia_peer(node_id1, '1.2.3.4', 4444))\n            self.assertEqual(0, len(peer1.ping_queue._pending_contacts))\n            pong = await peer1_from_peer3.ping()\n            self.assertEqual(b'pong', pong)\n            self.assertEqual(1, len(peer1.ping_queue._pending_contacts))\n            peer1.ping_queue._pending_contacts.clear()\n\n            # peers who are already good should be added\n            peer1_from_peer4 = peer4.get_rpc_peer(make_kademlia_peer(node_id1, '1.2.3.4', 4444))\n            peer1.peer_manager.update_contact_triple(node_id4,'1.2.3.7', 4444)\n            peer1.peer_manager.report_last_replied('1.2.3.7', 4444)\n            self.assertEqual(0, len(peer1.ping_queue._pending_contacts))\n            pong = await peer1_from_peer4.ping()\n            self.assertEqual(b'pong', pong)\n            await asyncio.sleep(0.5)\n            self.assertEqual(1, len(peer1.routing_table.get_peers()))\n            self.assertEqual(0, len(peer1.ping_queue._pending_contacts))\n            peer1.routing_table.buckets[0].peers.clear()\n\n            # peers who are known to be bad recently should not be added or maybe-pinged\n            peer1_from_peer4 = peer4.get_rpc_peer(make_kademlia_peer(node_id1, '1.2.3.4', 4444))\n            peer1.peer_manager.update_contact_triple(node_id4,'1.2.3.7', 4444)\n            peer1.peer_manager.report_failure('1.2.3.7', 4444)\n            peer1.peer_manager.report_failure('1.2.3.7', 4444)\n            self.assertEqual(0, len(peer1.ping_queue._pending_contacts))\n            pong = await peer1_from_peer4.ping()\n            self.assertEqual(b'pong', pong)\n            self.assertEqual(0, len(peer1.routing_table.get_peers()))\n            self.assertEqual(0, len(peer1.ping_queue._pending_contacts))\n\n            for p in [peer1, peer2, peer3, peer4]:\n                p.stop()\n                p.disconnect()\n"
  },
  {
    "path": "tests/unit/dht/protocol/test_routing_table.py",
    "content": "import asyncio\nfrom lbry.testcase import AsyncioTestCase\nfrom tests import dht_mocks\nfrom lbry.dht import constants\nfrom lbry.dht.node import Node\nfrom lbry.dht.peer import PeerManager, make_kademlia_peer\n\nexpected_ranges = [\n    (\n        0,\n        2462625387274654950767440006258975862817483704404090416746768337765357610718575663213391640930307227550414249394176\n    ),\n    (\n        2462625387274654950767440006258975862817483704404090416746768337765357610718575663213391640930307227550414249394176,\n        4925250774549309901534880012517951725634967408808180833493536675530715221437151326426783281860614455100828498788352\n    ),\n    (\n        4925250774549309901534880012517951725634967408808180833493536675530715221437151326426783281860614455100828498788352,\n        9850501549098619803069760025035903451269934817616361666987073351061430442874302652853566563721228910201656997576704\n    ),\n    (\n        9850501549098619803069760025035903451269934817616361666987073351061430442874302652853566563721228910201656997576704,\n        19701003098197239606139520050071806902539869635232723333974146702122860885748605305707133127442457820403313995153408\n    ),\n    (\n        19701003098197239606139520050071806902539869635232723333974146702122860885748605305707133127442457820403313995153408,\n        39402006196394479212279040100143613805079739270465446667948293404245721771497210611414266254884915640806627990306816\n    )\n]\n\n\nclass TestRouting(AsyncioTestCase):\n    async def test_fill_one_bucket(self):\n        loop = asyncio.get_event_loop()\n        peer_addresses = [\n            (constants.generate_id(1), '1.2.3.1'),\n            (constants.generate_id(2), '1.2.3.2'),\n            (constants.generate_id(3), '1.2.3.3'),\n            (constants.generate_id(4), '1.2.3.4'),\n            (constants.generate_id(5), '1.2.3.5'),\n            (constants.generate_id(6), '1.2.3.6'),\n            (constants.generate_id(7), '1.2.3.7'),\n            (constants.generate_id(8), '1.2.3.8'),\n            (constants.generate_id(9), '1.2.3.9'),\n        ]\n        with dht_mocks.mock_network_loop(loop):\n            nodes = {\n                i: Node(loop, PeerManager(loop), node_id, 4444, 4444, 3333, address)\n                for i, (node_id, address) in enumerate(peer_addresses)\n            }\n            node_1 = nodes[0]\n            contact_cnt = 0\n            for i in range(1, len(peer_addresses)):\n                self.assertEqual(len(node_1.protocol.routing_table.get_peers()), contact_cnt)\n                node = nodes[i]\n                peer = make_kademlia_peer(\n                    node.protocol.node_id, node.protocol.external_ip,\n                    udp_port=node.protocol.udp_port\n                )\n                added = await node_1.protocol._add_peer(peer)\n                self.assertTrue(added)\n                contact_cnt += 1\n\n            self.assertEqual(len(node_1.protocol.routing_table.get_peers()), 8)\n            self.assertEqual(node_1.protocol.routing_table.buckets_with_contacts(), 1)\n            for node in nodes.values():\n                node.protocol.stop()\n\n    async def test_cant_add_peer_without_a_node_id_gracefully(self):\n        loop = asyncio.get_event_loop()\n        node = Node(loop, PeerManager(loop), constants.generate_id(), 4444, 4444, 3333, '1.2.3.4')\n        bad_peer = make_kademlia_peer(None, '1.2.3.4', 5555)\n        with self.assertLogs(level='WARNING') as logged:\n            self.assertFalse(await node.protocol._add_peer(bad_peer))\n            self.assertEqual(1, len(logged.output))\n            self.assertTrue(logged.output[0].endswith('Tried adding a peer with no node id!'))\n\n\n    async def test_split_buckets(self):\n        loop = asyncio.get_event_loop()\n        peer_addresses = [\n            (constants.generate_id(1), '1.2.3.1'),\n        ]\n        for i in range(2, 200):\n            peer_addresses.append((constants.generate_id(i), f'1.2.3.{i}'))\n        with dht_mocks.mock_network_loop(loop):\n            nodes = {\n                i: Node(loop, PeerManager(loop), node_id, 4444, 4444, 3333, address)\n                for i, (node_id, address) in enumerate(peer_addresses)\n            }\n            node_1 = nodes[0]\n            for i in range(1, len(peer_addresses)):\n                node = nodes[i]\n                peer = make_kademlia_peer(\n                    node.protocol.node_id, node.protocol.external_ip,\n                    udp_port=node.protocol.udp_port\n                )\n                # set all of the peers to good (as to not attempt pinging stale ones during split)\n                node_1.protocol.peer_manager.report_last_replied(peer.address, peer.udp_port)\n                node_1.protocol.peer_manager.report_last_replied(peer.address, peer.udp_port)\n                await node_1.protocol._add_peer(peer)\n                # check that bucket 0 is always the one covering the local node id\n                self.assertEqual(True, node_1.protocol.routing_table.buckets[0].key_in_range(node_1.protocol.node_id))\n            self.assertEqual(40, len(node_1.protocol.routing_table.get_peers()))\n            self.assertEqual(len(expected_ranges), len(node_1.protocol.routing_table.buckets))\n            covered = 0\n            for (expected_min, expected_max), bucket in zip(expected_ranges, node_1.protocol.routing_table.buckets):\n                self.assertEqual(expected_min, bucket.range_min)\n                self.assertEqual(expected_max, bucket.range_max)\n                covered += bucket.range_max - bucket.range_min\n            self.assertEqual(2**384, covered)\n            for node in nodes.values():\n                node.stop()\n\n\n# from binascii import hexlify, unhexlify\n#\n# from twisted.trial import unittest\n# from twisted.internet import defer\n# from lbry.dht import constants\n# from lbry.dht.routingtable import TreeRoutingTable\n# from lbry.dht.contact import ContactManager\n# from lbry.dht.distance import Distance\n# from lbry.utils import generate_id\n#\n#\n# class FakeRPCProtocol:\n#     \"\"\" Fake RPC protocol; allows lbry.dht.contact.Contact objects to \"send\" RPCs \"\"\"\n#     def sendRPC(self, *args, **kwargs):\n#         return defer.succeed(None)\n#\n#\n# class TreeRoutingTableTest(unittest.TestCase):\n#     \"\"\" Test case for the RoutingTable class \"\"\"\n#     def setUp(self):\n#         self.contact_manager = ContactManager()\n#         self.nodeID = generate_id(b'node1')\n#         self.protocol = FakeRPCProtocol()\n#         self.routingTable = TreeRoutingTable(self.nodeID)\n#\n#     def test_distance(self):\n#         \"\"\" Test to see if distance method returns correct result\"\"\"\n#         d = Distance(bytes((170,) * 48))\n#         result = d(bytes((85,) * 48))\n#         expected = int(hexlify(bytes((255,) * 48)), 16)\n#         self.assertEqual(result, expected)\n#\n#     @defer.inlineCallbacks\n#     def test_add_contact(self):\n#         \"\"\" Tests if a contact can be added and retrieved correctly \"\"\"\n#         # Create the contact\n#         contact_id = generate_id(b'node2')\n#         contact = self.contact_manager.make_contact(contact_id, '127.0.0.1', 9182, self.protocol)\n#         # Now add it...\n#         yield self.routingTable.addContact(contact)\n#         # ...and request the closest nodes to it (will retrieve it)\n#         closest_nodes = self.routingTable.findCloseNodes(contact_id)\n#         self.assertEqual(len(closest_nodes), 1)\n#         self.assertIn(contact, closest_nodes)\n#\n#     @defer.inlineCallbacks\n#     def test_get_contact(self):\n#         \"\"\" Tests if a specific existing contact can be retrieved correctly \"\"\"\n#         contact_id = generate_id(b'node2')\n#         contact = self.contact_manager.make_contact(contact_id, '127.0.0.1', 9182, self.protocol)\n#         # Now add it...\n#         yield self.routingTable.addContact(contact)\n#         # ...and get it again\n#         same_contact = self.routingTable.getContact(contact_id)\n#         self.assertEqual(contact, same_contact, 'getContact() should return the same contact')\n#\n#     @defer.inlineCallbacks\n#     def test_add_parent_node_as_contact(self):\n#         \"\"\"\n#         Tests the routing table's behaviour when attempting to add its parent node as a contact\n#         \"\"\"\n#         # Create a contact with the same ID as the local node's ID\n#         contact = self.contact_manager.make_contact(self.nodeID, '127.0.0.1', 9182, self.protocol)\n#         # Now try to add it\n#         yield self.routingTable.addContact(contact)\n#         # ...and request the closest nodes to it using FIND_NODE\n#         closest_nodes = self.routingTable.findCloseNodes(self.nodeID, constants.k)\n#         self.assertNotIn(contact, closest_nodes, 'Node added itself as a contact')\n#\n#     @defer.inlineCallbacks\n#     def test_remove_contact(self):\n#         \"\"\" Tests contact removal \"\"\"\n#         # Create the contact\n#         contact_id = generate_id(b'node2')\n#         contact = self.contact_manager.make_contact(contact_id, '127.0.0.1', 9182, self.protocol)\n#         # Now add it...\n#         yield self.routingTable.addContact(contact)\n#         # Verify addition\n#         self.assertEqual(len(self.routingTable._buckets[0]), 1, 'Contact not added properly')\n#         # Now remove it\n#         self.routingTable.removeContact(contact)\n#         self.assertEqual(len(self.routingTable._buckets[0]), 0, 'Contact not removed properly')\n#\n#     @defer.inlineCallbacks\n#     def test_split_bucket(self):\n#         \"\"\" Tests if the the routing table correctly dynamically splits k-buckets \"\"\"\n#         self.assertEqual(self.routingTable._buckets[0].rangeMax, 2**384,\n#                              'Initial k-bucket range should be 0 <= range < 2**384')\n#         # Add k contacts\n#         for i in range(constants.k):\n#             node_id = generate_id(b'remote node %d' % i)\n#             contact = self.contact_manager.make_contact(node_id, '127.0.0.1', 9182, self.protocol)\n#             yield self.routingTable.addContact(contact)\n#\n#         self.assertEqual(len(self.routingTable._buckets), 1,\n#                              'Only k nodes have been added; the first k-bucket should now '\n#                              'be full, but should not yet be split')\n#         # Now add 1 more contact\n#         node_id = generate_id(b'yet another remote node')\n#         contact = self.contact_manager.make_contact(node_id, '127.0.0.1', 9182, self.protocol)\n#         yield self.routingTable.addContact(contact)\n#         self.assertEqual(len(self.routingTable._buckets), 2,\n#                              'k+1 nodes have been added; the first k-bucket should have been '\n#                              'split into two new buckets')\n#         self.assertNotEqual(self.routingTable._buckets[0].rangeMax, 2**384,\n#                          'K-bucket was split, but its range was not properly adjusted')\n#         self.assertEqual(self.routingTable._buckets[1].rangeMax, 2**384,\n#                              'K-bucket was split, but the second (new) bucket\\'s '\n#                              'max range was not set properly')\n#         self.assertEqual(self.routingTable._buckets[0].rangeMax,\n#                              self.routingTable._buckets[1].rangeMin,\n#                              'K-bucket was split, but the min/max ranges were '\n#                              'not divided properly')\n#\n#     @defer.inlineCallbacks\n#     def test_full_split(self):\n#         \"\"\"\n#         Test that a bucket is not split if it is full, but the new contact is not closer than the kth closest contact\n#         \"\"\"\n#\n#         self.routingTable._parentNodeID = bytes(48 * b'\\xff')\n#\n#         node_ids = [\n#             b\"100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\n#             b\"200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\n#             b\"300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\n#             b\"400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\n#             b\"500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\n#             b\"600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\n#             b\"700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\n#             b\"800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\n#             b\"ff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\n#             b\"010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"\n#         ]\n#\n#         # Add k contacts\n#         for nodeID in node_ids:\n#             # self.assertEquals(nodeID, node_ids[i].decode('hex'))\n#             contact = self.contact_manager.make_contact(unhexlify(nodeID), '127.0.0.1', 9182, self.protocol)\n#             yield self.routingTable.addContact(contact)\n#         self.assertEqual(len(self.routingTable._buckets), 2)\n#         self.assertEqual(len(self.routingTable._buckets[0]._contacts), 8)\n#         self.assertEqual(len(self.routingTable._buckets[1]._contacts), 2)\n#\n#         #  try adding a contact who is further from us than the k'th known contact\n#         nodeID = b'020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'\n#         nodeID = unhexlify(nodeID)\n#         contact = self.contact_manager.make_contact(nodeID, '127.0.0.1', 9182, self.protocol)\n#         self.assertFalse(self.routingTable._shouldSplit(self.routingTable._kbucketIndex(contact.id), contact.id))\n#         yield self.routingTable.addContact(contact)\n#         self.assertEqual(len(self.routingTable._buckets), 2)\n#         self.assertEqual(len(self.routingTable._buckets[0]._contacts), 8)\n#         self.assertEqual(len(self.routingTable._buckets[1]._contacts), 2)\n#         self.assertNotIn(contact, self.routingTable._buckets[0]._contacts)\n#         self.assertNotIn(contact, self.routingTable._buckets[1]._contacts)\n#\n\n# class KeyErrorFixedTest(unittest.TestCase):\n#     \"\"\" Basic tests case for boolean operators on the Contact class \"\"\"\n#\n#     def setUp(self):\n#         own_id = (2 ** constants.key_bits) - 1\n#         # carefully chosen own_id. here's the logic\n#         # we want a bunch of buckets (k+1, to be exact), and we want to make sure own_id\n#         # is not in bucket 0. so we put own_id at the end so we can keep splitting by adding to the\n#         # end\n#\n#         self.table = lbry.dht.routingtable.OptimizedTreeRoutingTable(own_id)\n#\n#     def fill_bucket(self, bucket_min):\n#         bucket_size = lbry.dht.constants.k\n#         for i in range(bucket_min, bucket_min + bucket_size):\n#             self.table.addContact(lbry.dht.contact.Contact(long(i), '127.0.0.1', 9999, None))\n#\n#     def overflow_bucket(self, bucket_min):\n#         bucket_size = lbry.dht.constants.k\n#         self.fill_bucket(bucket_min)\n#         self.table.addContact(\n#             lbry.dht.contact.Contact(long(bucket_min + bucket_size + 1),\n#                                         '127.0.0.1', 9999, None))\n#\n#     def testKeyError(self):\n#\n#         # find middle, so we know where bucket will split\n#         bucket_middle = self.table._buckets[0].rangeMax / 2\n#\n#         # fill last bucket\n#         self.fill_bucket(self.table._buckets[0].rangeMax - lbry.dht.constants.k - 1)\n#         # -1 in previous line because own_id is in last bucket\n#\n#         # fill/overflow 7 more buckets\n#         bucket_start = 0\n#         for i in range(0, lbry.dht.constants.k):\n#             self.overflow_bucket(bucket_start)\n#             bucket_start += bucket_middle / (2 ** i)\n#\n#         # replacement cache now has k-1 entries.\n#         # adding one more contact to bucket 0 used to cause a KeyError, but it should work\n#         self.table.addContact(\n#             lbry.dht.contact.Contact(long(lbry.dht.constants.k + 2), '127.0.0.1', 9999, None))\n#\n#         # import math\n#         # print \"\"\n#         # for i, bucket in enumerate(self.table._buckets):\n#         #     print \"Bucket \" + str(i) + \" (2 ** \" + str(\n#         #         math.log(bucket.rangeMin, 2) if bucket.rangeMin > 0 else 0) + \" <= x < 2 ** \"+str(\n#         #         math.log(bucket.rangeMax, 2)) + \")\"\n#         #     for c in bucket.getContacts():\n#         #         print \"  contact \" + str(c.id)\n#         # for key, bucket in self.table._replacementCache.items():\n#         #     print \"Replacement Cache for Bucket \" + str(key)\n#         #     for c in bucket:\n#         #         print \"  contact \" + str(c.id)\n"
  },
  {
    "path": "tests/unit/dht/serialization/__init__.py",
    "content": ""
  },
  {
    "path": "tests/unit/dht/serialization/test_bencoding.py",
    "content": "import unittest\nfrom lbry.dht.serialization.bencoding import _bencode, bencode, bdecode, DecodeError\n\n\nclass EncodeDecodeTest(unittest.TestCase):\n    def test_fail_with_not_dict(self):\n        with self.assertRaises(TypeError):\n            bencode(1)\n        with self.assertRaises(TypeError):\n            bencode(b'derp')\n        with self.assertRaises(TypeError):\n            bencode('derp')\n        with self.assertRaises(TypeError):\n            bencode([b'derp'])\n        with self.assertRaises(TypeError):\n            bencode([object()])\n        with self.assertRaises(TypeError):\n            bencode({b'derp': object()})\n\n    def test_fail_bad_type(self):\n        with self.assertRaises(DecodeError):\n            bdecode(b'd4le', True)\n\n    def test_integer(self):\n        self.assertEqual(_bencode(42), b'i42e')\n        self.assertEqual(bdecode(b'i42e', True), 42)\n\n    def test_bytes(self):\n        self.assertEqual(_bencode(b''), b'0:')\n        self.assertEqual(_bencode(b'spam'), b'4:spam')\n        self.assertEqual(_bencode(b'4:spam'), b'6:4:spam')\n        self.assertEqual(_bencode(bytearray(b'spam')), b'4:spam')\n\n        self.assertEqual(bdecode(b'0:', True), b'')\n        self.assertEqual(bdecode(b'4:spam', True), b'spam')\n        self.assertEqual(bdecode(b'6:4:spam', True), b'4:spam')\n\n    def test_string(self):\n        self.assertEqual(_bencode(''), b'0:')\n        self.assertEqual(_bencode('spam'), b'4:spam')\n        self.assertEqual(_bencode('4:spam'), b'6:4:spam')\n\n    def test_list(self):\n        self.assertEqual(_bencode([b'spam', 42]), b'l4:spami42ee')\n        self.assertEqual(bdecode(b'l4:spami42ee', True), [b'spam', 42])\n\n    def test_dict(self):\n        self.assertEqual(bencode({b'foo': 42, b'bar': b'spam'}), b'd3:bar4:spam3:fooi42ee')\n        self.assertEqual(bdecode(b'd3:bar4:spam3:fooi42ee'), {b'foo': 42, b'bar': b'spam'})\n\n    def test_mixed(self):\n        self.assertEqual(_bencode(\n            [[b'abc', b'127.0.0.1', 1919], [b'def', b'127.0.0.1', 1921]]),\n            b'll3:abc9:127.0.0.1i1919eel3:def9:127.0.0.1i1921eee'\n        )\n\n        self.assertEqual(bdecode(\n            b'll3:abc9:127.0.0.1i1919eel3:def9:127.0.0.1i1921eee', True),\n            [[b'abc', b'127.0.0.1', 1919], [b'def', b'127.0.0.1', 1921]]\n        )\n\n    def test_decode_error(self):\n        self.assertRaises(DecodeError, bdecode, b'abcdefghijklmnopqrstuvwxyz', True)\n        self.assertRaises(DecodeError, bdecode, b'', True)\n        self.assertRaises(DecodeError, bdecode, b'l4:spami42ee')\n"
  },
  {
    "path": "tests/unit/dht/serialization/test_datagram.py",
    "content": "import binascii\nimport unittest\nfrom lbry.dht.error import DecodeError\nfrom lbry.dht.serialization.bencoding import _bencode\nfrom lbry.dht.serialization.datagram import RequestDatagram, ResponseDatagram, decode_datagram, ErrorDatagram\nfrom lbry.dht.serialization.datagram import _decode_datagram\nfrom lbry.dht.serialization.datagram import REQUEST_TYPE, RESPONSE_TYPE, ERROR_TYPE\nfrom lbry.dht.serialization.datagram import make_compact_address, decode_compact_address\n\n\nclass TestDatagram(unittest.TestCase):\n    def test_ping_request_datagram(self):\n        self.assertRaises(ValueError, RequestDatagram.make_ping, b'1' * 48, b'1' * 21)\n        self.assertRaises(ValueError, RequestDatagram.make_ping, b'1' * 47, b'1' * 20)\n        self.assertEqual(20, len(RequestDatagram.make_ping(b'1' * 48).rpc_id))\n        serialized = RequestDatagram.make_ping(b'1' * 48, b'1' * 20).bencode()\n        decoded = decode_datagram(serialized)\n        self.assertEqual(decoded.packet_type, REQUEST_TYPE)\n        self.assertEqual(decoded.rpc_id, b'1' * 20)\n        self.assertEqual(decoded.node_id, b'1' * 48)\n        self.assertEqual(decoded.method, b'ping')\n        self.assertListEqual(decoded.args, [{b'protocolVersion': 1}])\n\n    def test_ping_response(self):\n        self.assertRaises(ValueError, ResponseDatagram, RESPONSE_TYPE, b'1' * 21, b'1' * 48, b'pong')\n        self.assertRaises(ValueError, ResponseDatagram, RESPONSE_TYPE, b'1' * 20, b'1' * 49, b'pong')\n        self.assertRaises(ValueError, ResponseDatagram, 5, b'1' * 20, b'1' * 48, b'pong')\n        serialized = ResponseDatagram(RESPONSE_TYPE, b'1' * 20, b'1' * 48, b'pong').bencode()\n        decoded = decode_datagram(serialized)\n        self.assertEqual(decoded.packet_type, RESPONSE_TYPE)\n        self.assertEqual(decoded.rpc_id, b'1' * 20)\n        self.assertEqual(decoded.node_id, b'1' * 48)\n        self.assertEqual(decoded.response, b'pong')\n\n    def test_find_node_request_datagram(self):\n        self.assertRaises(ValueError, RequestDatagram.make_find_node, b'1' * 49, b'2' * 48, b'1' * 20)\n        self.assertRaises(ValueError, RequestDatagram.make_find_node, b'1' * 48, b'2' * 49, b'1' * 20)\n        self.assertRaises(ValueError, RequestDatagram.make_find_node, b'1' * 48, b'2' * 48, b'1' * 21)\n        self.assertEqual(20, len(RequestDatagram.make_find_node(b'1' * 48, b'2' * 48).rpc_id))\n\n        serialized = RequestDatagram.make_find_node(b'1' * 48, b'2' * 48, b'1' * 20).bencode()\n        decoded = decode_datagram(serialized)\n        self.assertEqual(decoded.packet_type, REQUEST_TYPE)\n        self.assertEqual(decoded.rpc_id, b'1' * 20)\n        self.assertEqual(decoded.node_id, b'1' * 48)\n        self.assertEqual(decoded.method, b'findNode')\n        self.assertListEqual(decoded.args, [b'2' * 48, {b'protocolVersion': 1}])\n\n    def test_find_node_response(self):\n        closest_response = [(b'3' * 48, '1.2.3.4', 1234)]\n        expected = [[b'3' * 48, b'1.2.3.4', 1234]]\n\n        serialized = ResponseDatagram(RESPONSE_TYPE, b'1' * 20, b'1' * 48, closest_response).bencode()\n        decoded = decode_datagram(serialized)\n        self.assertEqual(decoded.packet_type, RESPONSE_TYPE)\n        self.assertEqual(decoded.rpc_id, b'1' * 20)\n        self.assertEqual(decoded.node_id, b'1' * 48)\n        self.assertEqual(decoded.response, expected)\n\n    def test_find_value_request(self):\n        self.assertRaises(ValueError, RequestDatagram.make_find_value, b'1' * 49, b'2' * 48, b'1' * 20)\n        self.assertRaises(ValueError, RequestDatagram.make_find_value, b'1' * 48, b'2' * 49, b'1' * 20)\n        self.assertRaises(ValueError, RequestDatagram.make_find_value, b'1' * 48, b'2' * 48, b'1' * 21)\n        self.assertRaises(ValueError, RequestDatagram.make_find_value, b'1' * 48, b'2' * 48, b'1' * 20, -1)\n        self.assertEqual(20, len(RequestDatagram.make_find_value(b'1' * 48, b'2' * 48).rpc_id))\n\n        # default page argument\n        serialized = RequestDatagram.make_find_value(b'1' * 48, b'2' * 48, b'1' * 20).bencode()\n        decoded = decode_datagram(serialized)\n        self.assertEqual(decoded.packet_type, REQUEST_TYPE)\n        self.assertEqual(decoded.rpc_id, b'1' * 20)\n        self.assertEqual(decoded.node_id, b'1' * 48)\n        self.assertEqual(decoded.method, b'findValue')\n        self.assertListEqual(decoded.args, [b'2' * 48, {b'protocolVersion': 1, b'p': 0}])\n\n        # nondefault page argument\n        serialized = RequestDatagram.make_find_value(b'1' * 48, b'2' * 48, b'1' * 20, 1).bencode()\n        decoded = decode_datagram(serialized)\n        self.assertEqual(decoded.packet_type, REQUEST_TYPE)\n        self.assertEqual(decoded.rpc_id, b'1' * 20)\n        self.assertEqual(decoded.node_id, b'1' * 48)\n        self.assertEqual(decoded.method, b'findValue')\n        self.assertListEqual(decoded.args, [b'2' * 48, {b'protocolVersion': 1, b'p': 1}])\n\n    def test_find_value_response_without_pages_field(self):\n        found_value_response = {b'2' * 48: [b'\\x7f\\x00\\x00\\x01']}\n        serialized = ResponseDatagram(RESPONSE_TYPE, b'1' * 20, b'1' * 48, found_value_response).bencode()\n        decoded = decode_datagram(serialized)\n        self.assertEqual(decoded.packet_type, RESPONSE_TYPE)\n        self.assertEqual(decoded.rpc_id, b'1' * 20)\n        self.assertEqual(decoded.node_id, b'1' * 48)\n        self.assertDictEqual(decoded.response, found_value_response)\n\n    def test_find_value_response_with_pages_field(self):\n        found_value_response = {b'2' * 48: [b'\\x7f\\x00\\x00\\x01'], b'p': 1}\n        serialized = ResponseDatagram(RESPONSE_TYPE, b'1' * 20, b'1' * 48, found_value_response).bencode()\n        decoded = decode_datagram(serialized)\n        self.assertEqual(decoded.packet_type, RESPONSE_TYPE)\n        self.assertEqual(decoded.rpc_id, b'1' * 20)\n        self.assertEqual(decoded.node_id, b'1' * 48)\n        self.assertDictEqual(decoded.response, found_value_response)\n\n    def test_store_request(self):\n        self.assertRaises(ValueError, RequestDatagram.make_store, b'1' * 47, b'2' * 48, b'3' * 48, 3333, b'1' * 20)\n        self.assertRaises(ValueError, RequestDatagram.make_store, b'1' * 48, b'2' * 49, b'3' * 48, 3333, b'1' * 20)\n        self.assertRaises(ValueError, RequestDatagram.make_store, b'1' * 48, b'2' * 48, b'3' * 47, 3333, b'1' * 20)\n        self.assertRaises(ValueError, RequestDatagram.make_store, b'1' * 48, b'2' * 48, b'3' * 48, -3333, b'1' * 20)\n        self.assertRaises(ValueError, RequestDatagram.make_store, b'1' * 48, b'2' * 48, b'3' * 48, 3333, b'1' * 21)\n\n        serialized = RequestDatagram.make_store(b'1' * 48, b'2' * 48, b'3' * 48, 3333, b'1' * 20).bencode()\n        decoded = decode_datagram(serialized)\n        self.assertEqual(decoded.packet_type, REQUEST_TYPE)\n        self.assertEqual(decoded.rpc_id, b'1' * 20)\n        self.assertEqual(decoded.node_id, b'1' * 48)\n        self.assertEqual(decoded.method, b'store')\n\n    def test_error_datagram(self):\n        serialized = ErrorDatagram(ERROR_TYPE, b'1' * 20, b'1' * 48, b'FakeErrorType', b'more info').bencode()\n        decoded = decode_datagram(serialized)\n        self.assertEqual(decoded.packet_type, ERROR_TYPE)\n        self.assertEqual(decoded.rpc_id, b'1' * 20)\n        self.assertEqual(decoded.node_id, b'1' * 48)\n        self.assertEqual(decoded.exception_type, 'FakeErrorType')\n        self.assertEqual(decoded.response, 'more info')\n\n    def test_invalid_datagram_type(self):\n        serialized = b'di0ei5ei1e20:11111111111111111111i2e48:11111111111111111111' \\\n                     b'1111111111111111111111111111i3e13:FakeErrorTypei4e9:more infoe'\n        self.assertRaises(ValueError, decode_datagram, serialized)\n        self.assertRaises(DecodeError, decode_datagram, _bencode([1, 2, 3, 4]))\n\n    def test_optional_field_backwards_compatible(self):\n        datagram = decode_datagram(_bencode({\n            0: 0,\n            1: b'\\n\\xbc\\xb5&\\x9dl\\xfc\\x1e\\x87\\xa0\\x8e\\x92\\x0b\\xf3\\x9f\\xe9\\xdf\\x8e\\x92\\xfc',\n            2: b'111111111111111111111111111111111111111111111111',\n            3: b'ping',\n            4: [{b'protocolVersion': 1}],\n            5: b'should not error'\n        }))\n        self.assertEqual(datagram.packet_type, REQUEST_TYPE)\n        self.assertEqual(b'ping', datagram.method)\n\n    def test_str_or_int_keys(self):\n        datagram = decode_datagram(_bencode({\n            b'0': 0,\n            b'1': b'\\n\\xbc\\xb5&\\x9dl\\xfc\\x1e\\x87\\xa0\\x8e\\x92\\x0b\\xf3\\x9f\\xe9\\xdf\\x8e\\x92\\xfc',\n            b'2': b'111111111111111111111111111111111111111111111111',\n            b'3': b'ping',\n            b'4': [{b'protocolVersion': 1}],\n            b'5': b'should not error'\n        }))\n        self.assertEqual(datagram.packet_type, REQUEST_TYPE)\n        self.assertEqual(b'ping', datagram.method)\n\n    def test_mixed_str_or_int_keys(self):\n        # datagram, _ = _bencode({\n        #     b'0': 0,\n        #     1: b'\\n\\xbc\\xb5&\\x9dl\\xfc\\x1e\\x87\\xa0\\x8e\\x92\\x0b\\xf3\\x9f\\xe9\\xdf\\x8e\\x92\\xfc',\n        #     b'2': b'111111111111111111111111111111111111111111111111',\n        #     3: b'ping',\n        #     b'4': [{b'protocolVersion': 1}],\n        #     b'5': b'should not error'\n        # }))\n        encoded = binascii.unhexlify(b\"64313a3069306569316532303a0abcb5269d6cfc1e87a08e920bf39fe9df8e92fc313a3234383a313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131693365343a70696e67313a346c6431353a70726f746f636f6c56657273696f6e6931656565313a3531363a73686f756c64206e6f74206572726f7265\")\n        self.assertDictEqual(\n            {\n             'packet_type': 0,\n             'rpc_id': b'\\n\\xbc\\xb5&\\x9dl\\xfc\\x1e\\x87\\xa0\\x8e\\x92\\x0b\\xf3\\x9f\\xe9\\xdf\\x8e\\x92\\xfc',\n             'node_id': b'111111111111111111111111111111111111111111111111',\n             'method': b'ping',\n             'args': [{b'protocolVersion': 1}]\n            }, _decode_datagram(encoded)[0]\n        )\n\n\nclass TestCompactAddress(unittest.TestCase):\n    def test_encode_decode(self, address='1.2.3.4', port=4444, node_id=b'1' * 48):\n        decoded = decode_compact_address(make_compact_address(node_id, address, port))\n        self.assertEqual((node_id, address, port), decoded)\n\n    def test_errors(self):\n        self.assertRaises(ValueError, make_compact_address, b'1' * 48, '1.2.3.4', 0)\n        self.assertRaises(ValueError, make_compact_address, b'1' * 48, '1.2.3.4', 65536)\n        self.assertRaises(\n            ValueError, decode_compact_address,\n            b'\\x01\\x02\\x03\\x04\\x00\\x00111111111111111111111111111111111111111111111111'\n        )\n\n        self.assertRaises(ValueError, make_compact_address, b'1' * 48, '1.2.3.4.5', 4444)\n        self.assertRaises(ValueError, make_compact_address, b'1' * 47, '1.2.3.4', 4444)\n        self.assertRaises(\n            ValueError, decode_compact_address,\n            b'\\x01\\x02\\x03\\x04\\x11\\\\11111111111111111111111111111111111111111111111'\n        )\n"
  },
  {
    "path": "tests/unit/dht/test_blob_announcer.py",
    "content": "import contextlib\nimport logging\nimport typing\nimport binascii\nimport socket\nimport asyncio\n\nfrom lbry.testcase import AsyncioTestCase\nfrom tests import dht_mocks\nfrom lbry.dht.protocol.distance import Distance\nfrom lbry.conf import Config\nfrom lbry.dht import constants\nfrom lbry.dht.node import Node\nfrom lbry.dht.peer import PeerManager, make_kademlia_peer\nfrom lbry.dht.blob_announcer import BlobAnnouncer\nfrom lbry.extras.daemon.storage import SQLiteStorage\n\n\nclass TestBlobAnnouncer(AsyncioTestCase):\n    async def setup_node(self, peer_addresses, address, node_id):\n        self.nodes: typing.Dict[int, Node] = {}\n        self.advance = dht_mocks.get_time_accelerator(self.loop)\n        self.instant_advance = dht_mocks.get_time_accelerator(self.loop)\n        self.conf = Config()\n        self.peer_manager = PeerManager(self.loop)\n        self.node = Node(self.loop, self.peer_manager, node_id, 4444, 4444, 3333, address)\n        await self.node.start_listening(address)\n        await asyncio.gather(*[self.add_peer(node_id, address) for node_id, address in peer_addresses])\n        for first_peer in self.nodes.values():\n            for second_peer in self.nodes.values():\n                if first_peer == second_peer:\n                    continue\n                self.add_peer_to_routing_table(first_peer, second_peer)\n                self.add_peer_to_routing_table(second_peer, first_peer)\n        await self.advance(0.1)  # just to make pings go through\n        self.node.joined.set()\n        self.node._refresh_task = self.loop.create_task(self.node.refresh_node())\n        self.storage = SQLiteStorage(self.conf, \":memory:\", self.loop, self.loop.time)\n        await self.storage.open()\n        self.blob_announcer = BlobAnnouncer(self.loop, self.node, self.storage)\n\n    async def add_peer(self, node_id, address, add_to_routing_table=True):\n        #print('add', node_id.hex()[:8], address)\n        n = Node(self.loop, PeerManager(self.loop), node_id, 4444, 4444, 3333, address)\n        await n.start_listening(address)\n        self.nodes.update({len(self.nodes): n})\n        if add_to_routing_table:\n            self.add_peer_to_routing_table(self.node, n)\n\n    def add_peer_to_routing_table(self, adder, being_added):\n        adder.protocol.add_peer(\n            make_kademlia_peer(\n                being_added.protocol.node_id, being_added.protocol.external_ip, being_added.protocol.udp_port\n            )\n        )\n\n    @contextlib.asynccontextmanager\n    async def _test_network_context(self, peer_count=200):\n        self.peer_addresses = [\n            (constants.generate_id(i), socket.inet_ntoa(int(i + 0x01000001).to_bytes(length=4, byteorder='big')))\n            for i in range(1, peer_count + 1)\n        ]\n        try:\n            with dht_mocks.mock_network_loop(self.loop):\n                await self.setup_node(self.peer_addresses, '1.2.3.1', constants.generate_id(1000))\n                yield\n        finally:\n            self.blob_announcer.stop()\n            self.node.stop()\n            for n in self.nodes.values():\n                n.stop()\n\n    async def chain_peer(self, node_id, address):\n        previous_last_node = self.nodes[len(self.nodes) - 1]\n        await self.add_peer(node_id, address, False)\n        last_node = self.nodes[len(self.nodes) - 1]\n        peer = last_node.protocol.get_rpc_peer(\n            make_kademlia_peer(\n                previous_last_node.protocol.node_id, previous_last_node.protocol.external_ip,\n                previous_last_node.protocol.udp_port\n            )\n        )\n        await peer.ping()\n        return last_node\n\n    async def test_announce_blobs(self):\n        blob1 = binascii.hexlify(b'1' * 48).decode()\n        blob2 = binascii.hexlify(b'2' * 48).decode()\n\n        async with self._test_network_context(peer_count=100):\n            await self.storage.add_blobs((blob1, 1024, 0, True), (blob2, 1024, 0, True), finished=True)\n            await self.storage.add_blobs(\n                *((constants.generate_id(value).hex(), 1024, 0, True) for value in range(1000, 1090)),\n                finished=True)\n            await self.storage.db.execute(\"update blob set next_announce_time=0, should_announce=1\")\n            to_announce = await self.storage.get_blobs_to_announce()\n            self.assertEqual(92, len(to_announce))\n            self.blob_announcer.start(batch_size=10)  # so it covers batching logic\n            # takes 60 seconds to start, but we advance 120 to ensure it processed all batches\n            ongoing_announcements = asyncio.ensure_future(self.blob_announcer.wait())\n            await self.instant_advance(60.0)\n            await ongoing_announcements\n            to_announce = await self.storage.get_blobs_to_announce()\n            self.assertEqual(0, len(to_announce))\n            self.blob_announcer.stop()\n\n            # as routing table pollution will cause some peers to be hard to reach, we add a tolerance for CI\n            tolerance = 0.8  # at least 80% of the announcements are within the top K\n            for blob in await self.storage.get_all_blob_hashes():\n                distance = Distance(bytes.fromhex(blob))\n                candidates = list(self.nodes.values())\n                candidates.sort(key=lambda sorting_node: distance(sorting_node.protocol.node_id))\n                has_it = 0\n                for index, node in enumerate(candidates[:constants.K], start=1):\n                    if node.protocol.data_store.get_peers_for_blob(bytes.fromhex(blob)):\n                        has_it += 1\n                    else:\n                        logging.warning(\"blob %s wasnt found between the best K (%s)\", blob[:8], node.protocol.node_id.hex()[:8])\n                self.assertGreaterEqual(has_it, int(tolerance * constants.K))\n\n\n            # test that we can route from a poorly connected peer all the way to the announced blob\n\n            current = len(self.nodes)\n            await self.chain_peer(constants.generate_id(current + 1), '1.2.3.10')\n            await self.chain_peer(constants.generate_id(current + 2), '1.2.3.11')\n            await self.chain_peer(constants.generate_id(current + 3), '1.2.3.12')\n            await self.chain_peer(constants.generate_id(current + 4), '1.2.3.13')\n            last = await self.chain_peer(constants.generate_id(current + 5), '1.2.3.14')\n\n            search_q, peer_q = asyncio.Queue(), asyncio.Queue()\n            search_q.put_nowait(blob1)\n\n            _, task = last.accumulate_peers(search_q, peer_q)\n            found_peers = await asyncio.wait_for(peer_q.get(), 1.0)\n            task.cancel()\n\n            self.assertEqual(1, len(found_peers))\n            self.assertEqual(self.node.protocol.node_id, found_peers[0].node_id)\n            self.assertEqual(self.node.protocol.external_ip, found_peers[0].address)\n            self.assertEqual(self.node.protocol.peer_port, found_peers[0].tcp_port)\n\n    async def test_popular_blob(self):\n        peer_count = 150\n        blob_hash = constants.generate_id(99999)\n\n        async with self._test_network_context(peer_count=peer_count):\n            total_seen = set()\n            announced_to = self.nodes.pop(0)\n            for i, node in enumerate(self.nodes.values()):\n                self.add_peer_to_routing_table(announced_to, node)\n                peer = node.protocol.get_rpc_peer(\n                    make_kademlia_peer(\n                        announced_to.protocol.node_id,\n                        announced_to.protocol.external_ip,\n                        announced_to.protocol.udp_port\n                    )\n                )\n                response = await peer.store(blob_hash)\n                self.assertEqual(response, b'OK')\n                peers_for_blob = await peer.find_value(blob_hash, 0)\n                if i == 0:\n                    self.assertNotIn(blob_hash, peers_for_blob)\n                    self.assertEqual(peers_for_blob[b'p'], 0)\n                else:\n                    self.assertEqual(len(peers_for_blob[blob_hash]), min(i, constants.K))\n                    self.assertEqual(len(announced_to.protocol.data_store.get_peers_for_blob(blob_hash)), i + 1)\n                if i - 1 > constants.K:\n                    self.assertEqual(len(peers_for_blob[b'contacts']), constants.K)\n                    self.assertEqual(peers_for_blob[b'p'], (i // (constants.K + 1)) + 1)\n                    seen = set(peers_for_blob[blob_hash])\n                    self.assertEqual(len(seen), constants.K)\n                    self.assertEqual(len(peers_for_blob[blob_hash]), len(seen))\n\n                    for pg in range(1, peers_for_blob[b'p']):\n                        page_x = await peer.find_value(blob_hash, pg)\n                        self.assertNotIn(b'contacts', page_x)\n                        page_x_set = set(page_x[blob_hash])\n                        self.assertEqual(len(page_x[blob_hash]), len(page_x_set))\n                        self.assertGreater(len(page_x_set), 0)\n                        self.assertSetEqual(seen.intersection(page_x_set), set())\n                        seen.intersection_update(page_x_set)\n                        total_seen.update(page_x_set)\n                else:\n                    self.assertEqual(len(peers_for_blob[b'contacts']), 8)  # we always add 8 on first page\n            self.assertEqual(len(total_seen), peer_count - 2)\n"
  },
  {
    "path": "tests/unit/dht/test_node.py",
    "content": "import asyncio\nimport time\nimport unittest\nimport typing\nfrom lbry.testcase import AsyncioTestCase\nfrom tests import dht_mocks\nfrom lbry.conf import Config\nfrom lbry.dht import constants\nfrom lbry.dht.node import Node\nfrom lbry.dht.peer import PeerManager, make_kademlia_peer\nfrom lbry.extras.daemon.storage import SQLiteStorage\n\n\nclass TestBootstrapNode(AsyncioTestCase):\n    TIMEOUT = 10.0  # do not increase. Hitting a timeout is a real failure\n\n    async def test_bootstrap_node_adds_all_peers(self):\n        loop = asyncio.get_event_loop()\n        loop.set_debug(False)\n\n        with dht_mocks.mock_network_loop(loop):\n            advance = dht_mocks.get_time_accelerator(loop)\n            self.bootstrap_node = Node(self.loop, PeerManager(loop), constants.generate_id(),\n                                       4444, 4444, 3333, '1.2.3.4', is_bootstrap_node=True)\n            self.bootstrap_node.start('1.2.3.4', [])\n            self.bootstrap_node.protocol.ping_queue._default_delay = 0\n            self.addCleanup(self.bootstrap_node.stop)\n\n            # start the nodes\n            nodes = {}\n            futs = []\n            for i in range(100):\n                nodes[i] = Node(loop, PeerManager(loop), constants.generate_id(i), 4444, 4444, 3333, f'1.3.3.{i}')\n                nodes[i].start(f'1.3.3.{i}', [('1.2.3.4', 4444)])\n                self.addCleanup(nodes[i].stop)\n                futs.append(nodes[i].joined.wait())\n            await asyncio.gather(*futs)\n            while self.bootstrap_node.protocol.ping_queue.busy:\n                await advance(1)\n            self.assertEqual(100, len(self.bootstrap_node.protocol.routing_table.get_peers()))\n\n\nclass TestNodePingQueueDiscover(AsyncioTestCase):\n    async def test_ping_queue_discover(self):\n        loop = asyncio.get_event_loop()\n        loop.set_debug(False)\n\n        peer_addresses = [\n            (constants.generate_id(1), '1.2.3.1'),\n            (constants.generate_id(2), '1.2.3.2'),\n            (constants.generate_id(3), '1.2.3.3'),\n            (constants.generate_id(4), '1.2.3.4'),\n            (constants.generate_id(5), '1.2.3.5'),\n            (constants.generate_id(6), '1.2.3.6'),\n            (constants.generate_id(7), '1.2.3.7'),\n            (constants.generate_id(8), '1.2.3.8'),\n            (constants.generate_id(9), '1.2.3.9'),\n        ]\n        with dht_mocks.mock_network_loop(loop):\n            advance = dht_mocks.get_time_accelerator(loop)\n            # start the nodes\n            nodes: typing.Dict[int, Node] = {\n                i: Node(loop, PeerManager(loop), node_id, 4444, 4444, 3333, address)\n                for i, (node_id, address) in enumerate(peer_addresses)\n            }\n            for i, n in nodes.items():\n                n.start(peer_addresses[i][1], [])\n\n            await advance(1)\n\n            node_1 = nodes[0]\n\n            # ping 8 nodes from node_1, this will result in a delayed return ping\n            futs = []\n            for i in range(1, len(peer_addresses)):\n                node = nodes[i]\n                assert node.protocol.node_id != node_1.protocol.node_id\n                peer = make_kademlia_peer(\n                    node.protocol.node_id, node.protocol.external_ip, udp_port=node.protocol.udp_port\n                )\n                futs.append(node_1.protocol.get_rpc_peer(peer).ping())\n            await advance(3)\n            replies = await asyncio.gather(*tuple(futs))\n            self.assertTrue(all(map(lambda reply: reply == b\"pong\", replies)))\n\n            # run for long enough for the delayed pings to have been sent by node 1\n            await advance(1000)\n\n            # verify all of the previously pinged peers have node_1 in their routing tables\n            for n in nodes.values():\n                peers = n.protocol.routing_table.get_peers()\n                if n is node_1:\n                    self.assertEqual(8, len(peers))\n                # TODO: figure out why this breaks\n                # else:\n                #     self.assertEqual(1, len(peers))\n                #     self.assertEqual((peers[0].node_id, peers[0].address, peers[0].udp_port),\n                #                      (node_1.protocol.node_id, node_1.protocol.external_ip, node_1.protocol.udp_port))\n\n            # run long enough for the refresh loop to run\n            await advance(3600)\n\n            # verify all the nodes know about each other\n            for n in nodes.values():\n                if n is node_1:\n                    continue\n                peers = n.protocol.routing_table.get_peers()\n                self.assertEqual(8, len(peers))\n                self.assertSetEqual(\n                    {n_id[0] for n_id in peer_addresses if n_id[0] != n.protocol.node_id},\n                    {c.node_id for c in peers}\n                )\n                self.assertSetEqual(\n                    {n_addr[1] for n_addr in peer_addresses if n_addr[1] != n.protocol.external_ip},\n                    {c.address for c in peers}\n                )\n\n            # teardown\n            for n in nodes.values():\n                n.stop()\n\n\nclass TestTemporarilyLosingConnection(AsyncioTestCase):\n    @unittest.SkipTest\n    async def test_losing_connection(self):\n        async def wait_for(check_ok, insist, timeout=20):\n            start = time.time()\n            while time.time() - start < timeout:\n                if check_ok():\n                    break\n                await asyncio.sleep(0)\n            else:\n                insist()\n\n        loop = self.loop\n        loop.set_debug(False)\n\n        peer_addresses = [\n            ('1.2.3.4', 40000+i) for i in range(10)\n        ]\n        node_ids = [constants.generate_id(i) for i in range(10)]\n\n        nodes = [\n            Node(\n                loop, PeerManager(loop), node_id, udp_port, udp_port, 3333, address,\n                storage=SQLiteStorage(Config(), \":memory:\", self.loop, self.loop.time)\n            )\n            for node_id, (address, udp_port) in zip(node_ids, peer_addresses)\n        ]\n        dht_network = {peer_addresses[i]: node.protocol for i, node in enumerate(nodes)}\n        num_seeds = 3\n\n        with dht_mocks.mock_network_loop(loop, dht_network):\n            for i, n in enumerate(nodes):\n                await n._storage.open()\n                self.addCleanup(n.stop)\n                n.start(peer_addresses[i][0], peer_addresses[:num_seeds])\n            await asyncio.gather(*[n.joined.wait() for n in nodes])\n\n            node = nodes[-1]\n            advance = dht_mocks.get_time_accelerator(loop)\n            await advance(500)\n\n            # Join the network, assert that at least the known peers are in RT\n            self.assertTrue(node.joined.is_set())\n            self.assertTrue(len(node.protocol.routing_table.get_peers()) >= num_seeds)\n\n            # Refresh, so that the peers are persisted\n            self.assertFalse(len(await node._storage.get_persisted_kademlia_peers()) > num_seeds)\n            await advance(4000)\n            self.assertTrue(len(await node._storage.get_persisted_kademlia_peers()) > num_seeds)\n\n            # We lost internet connection - all the peers stop responding\n            dht_network.pop((node.protocol.external_ip, node.protocol.udp_port))\n\n            # The peers are cleared on refresh from RT and storage\n            await advance(4000)\n            self.assertListEqual([], await node._storage.get_persisted_kademlia_peers())\n            await wait_for(\n                lambda: len(node.protocol.routing_table.get_peers()) == 0,\n                lambda: self.assertListEqual(node.protocol.routing_table.get_peers(), [])\n            )\n\n            # Reconnect\n            dht_network[(node.protocol.external_ip, node.protocol.udp_port)] = node.protocol\n\n            # Check that node reconnects at least to them\n            await advance(1000)\n            await wait_for(\n                lambda: len(node.protocol.routing_table.get_peers()) >= num_seeds,\n                lambda: self.assertGreaterEqual(len(node.protocol.routing_table.get_peers()), num_seeds)\n            )\n"
  },
  {
    "path": "tests/unit/dht/test_peer.py",
    "content": "import asyncio\nimport unittest\nfrom lbry.utils import generate_id\nfrom lbry.dht.peer import PeerManager, make_kademlia_peer, is_valid_public_ipv4\nfrom lbry.testcase import AsyncioTestCase\n\n\nclass PeerTest(AsyncioTestCase):\n    def setUp(self):\n        self.loop = asyncio.get_event_loop()\n        self.peer_manager = PeerManager(self.loop)\n        self.node_ids = [generate_id(), generate_id(), generate_id()]\n        self.first_contact = make_kademlia_peer(self.node_ids[1], '1.0.0.1', udp_port=1024)\n        self.second_contact = make_kademlia_peer(self.node_ids[0], '1.0.0.2', udp_port=1024)\n\n    def test_peer_is_good_unknown_peer(self):\n        # Scenario: peer replied, but caller doesn't know the node_id.\n        # Outcome: We can't say it's good or bad.\n        # (yes, we COULD tell the node id, but not here. It would be\n        # a side effect and the caller is responsible to discover it)\n        peer = make_kademlia_peer(None, '1.2.3.4', 4444)\n        self.peer_manager.report_last_requested('1.2.3.4', 4444)\n        self.peer_manager.report_last_replied('1.2.3.4', 4444)\n        self.assertIsNone(self.peer_manager.peer_is_good(peer))\n\n    def test_make_contact_error_cases(self):\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], '1.2.3.4', 100000)\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], '1.2.3.4.5', 1024)\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], 'this is not an ip', 1024)\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], '1.2.3.4', -1000)\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], '1.2.3.4', 0)\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], '1.2.3.4', 1023)\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], '1.2.3.4', 70000)\n        self.assertRaises(ValueError, make_kademlia_peer, b'not valid node id', '1.2.3.4', 1024)\n\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], '0.0.0.0', 1024)\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], '10.0.0.1', 1024)\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], '100.64.0.1', 1024)\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], '127.0.0.1', 1024)\n        self.assertIsNotNone(make_kademlia_peer(self.node_ids[1], '127.0.0.1', 1024, allow_localhost=True))\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], '192.168.0.1', 1024)\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], '172.16.0.1', 1024)\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], '169.254.1.1', 1024)\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], '192.0.0.2', 1024)\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], '192.0.2.2', 1024)\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], '192.88.99.2', 1024)\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], '198.18.1.1', 1024)\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], '198.51.100.2', 1024)\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], '198.51.100.2', 1024)\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], '203.0.113.4', 1024)\n        for i in range(32):\n            self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], f\"{224 + i}.0.0.0\", 1024)\n        self.assertRaises(ValueError, make_kademlia_peer, self.node_ids[1], '255.255.255.255', 1024)\n        self.assertRaises(\n            ValueError, make_kademlia_peer, self.node_ids[1], 'beee:eeee:eeee:eeee:eeee:eeee:eeee:eeef', 1024\n        )\n        self.assertRaises(\n            ValueError, make_kademlia_peer, self.node_ids[1], '2001:db8::ff00:42:8329', 1024\n        )\n\n    def test_is_valid_ipv4(self):\n        self.assertFalse(is_valid_public_ipv4('beee:eeee:eeee:eeee:eeee:eeee:eeee:eeef'))\n        self.assertFalse(is_valid_public_ipv4('beee:eeee:eeee:eeee:eeee:eeee:eeee:eeef', True))\n\n        self.assertFalse(is_valid_public_ipv4('2001:db8::ff00:42:8329'))\n        self.assertFalse(is_valid_public_ipv4('2001:db8::ff00:42:8329', True))\n\n        self.assertFalse(is_valid_public_ipv4('127.0.0.1'))\n        self.assertTrue(is_valid_public_ipv4('127.0.0.1', True))\n\n        self.assertFalse(is_valid_public_ipv4('172.16.0.1'))\n        self.assertFalse(is_valid_public_ipv4('172.16.0.1', True))\n\n        self.assertTrue(is_valid_public_ipv4('1.2.3.4'))\n        self.assertTrue(is_valid_public_ipv4('1.2.3.4', True))\n\n        self.assertFalse(is_valid_public_ipv4('derp'))\n        self.assertFalse(is_valid_public_ipv4('derp', True))\n\n    def test_boolean(self):\n        self.assertNotEqual(self.first_contact, self.second_contact)\n        self.assertEqual(\n            self.second_contact, make_kademlia_peer(self.node_ids[0], '1.0.0.2', udp_port=1024)\n        )\n\n    def test_compact_ip(self):\n        self.assertEqual(b'\\x01\\x00\\x00\\x01', self.first_contact.compact_ip())\n        self.assertEqual(b'\\x01\\x00\\x00\\x02', self.second_contact.compact_ip())\n\n\n@unittest.SkipTest\nclass TestContactLastReplied(unittest.TestCase):\n    def setUp(self):\n        self.clock = task.Clock()\n        self.contact_manager = ContactManager(self.clock.seconds)\n        self.contact = self.contact_manager.make_contact(generate_id(), \"127.0.0.1\", 4444, None)\n        self.clock.advance(3600)\n        self.assertIsNone(self.contact.contact_is_good)\n\n    def test_stale_replied_to_us(self):\n        self.contact.update_last_replied()\n        self.assertIs(self.contact.contact_is_good, True)\n\n    def test_stale_requested_from_us(self):\n        self.contact.update_last_requested()\n        self.assertIsNone(self.contact.contact_is_good)\n\n    def test_stale_then_fail(self):\n        self.contact.update_last_failed()\n        self.assertIsNone(self.contact.contact_is_good)\n        self.clock.advance(1)\n        self.contact.update_last_failed()\n        self.assertIs(self.contact.contact_is_good, False)\n\n    def test_good_turned_stale(self):\n        self.contact.update_last_replied()\n        self.assertIs(self.contact.contact_is_good, True)\n        self.clock.advance(constants.checkRefreshInterval - 1)\n        self.assertIs(self.contact.contact_is_good, True)\n        self.clock.advance(1)\n        self.assertIsNone(self.contact.contact_is_good)\n\n    def test_good_then_fail(self):\n        self.contact.update_last_replied()\n        self.assertIs(self.contact.contact_is_good, True)\n        self.clock.advance(1)\n        self.contact.update_last_failed()\n        self.assertIs(self.contact.contact_is_good, True)\n        self.clock.advance(59)\n        self.assertIs(self.contact.contact_is_good, True)\n        self.contact.update_last_failed()\n        self.assertIs(self.contact.contact_is_good, False)\n        for _ in range(7200):\n            self.clock.advance(60)\n            self.assertIs(self.contact.contact_is_good, False)\n\n    def test_good_then_fail_then_good(self):\n        # it replies\n        self.contact.update_last_replied()\n        self.assertIs(self.contact.contact_is_good, True)\n        self.clock.advance(1)\n\n        # it fails twice in a row\n        self.contact.update_last_failed()\n        self.clock.advance(1)\n        self.contact.update_last_failed()\n        self.assertIs(self.contact.contact_is_good, False)\n        self.clock.advance(1)\n\n        # it replies\n        self.contact.update_last_replied()\n        self.clock.advance(1)\n        self.assertIs(self.contact.contact_is_good, True)\n\n        # it goes stale\n        self.clock.advance(constants.checkRefreshInterval - 2)\n        self.assertIs(self.contact.contact_is_good, True)\n        self.clock.advance(1)\n        self.assertIsNone(self.contact.contact_is_good)\n\n\n@unittest.SkipTest\nclass TestContactLastRequested(unittest.TestCase):\n    def setUp(self):\n        self.clock = task.Clock()\n        self.contact_manager = ContactManager(self.clock.seconds)\n        self.contact = self.contact_manager.make_contact(generate_id(), \"127.0.0.1\", 4444, None)\n        self.clock.advance(1)\n        self.contact.update_last_replied()\n        self.clock.advance(3600)\n        self.assertIsNone(self.contact.contact_is_good)\n\n    def test_previous_replied_then_requested(self):\n        # it requests\n        self.contact.update_last_requested()\n        self.assertIs(self.contact.contact_is_good, True)\n\n        # it goes stale\n        self.clock.advance(constants.checkRefreshInterval - 1)\n        self.assertIs(self.contact.contact_is_good, True)\n        self.clock.advance(1)\n        self.assertIsNone(self.contact.contact_is_good)\n\n    def test_previous_replied_then_requested_then_failed(self):\n        # it requests\n        self.contact.update_last_requested()\n        self.assertIs(self.contact.contact_is_good, True)\n        self.clock.advance(1)\n\n        # it fails twice in a row\n        self.contact.update_last_failed()\n        self.clock.advance(1)\n        self.contact.update_last_failed()\n        self.assertIs(self.contact.contact_is_good, False)\n        self.clock.advance(1)\n\n        # it requests\n        self.contact.update_last_requested()\n        self.clock.advance(1)\n        self.assertIs(self.contact.contact_is_good, False)\n\n        # it goes stale\n        self.clock.advance((constants.refreshTimeout / 4) - 2)\n        self.assertIs(self.contact.contact_is_good, False)\n        self.clock.advance(1)\n        self.assertIs(self.contact.contact_is_good, False)\n"
  },
  {
    "path": "tests/unit/lbrynet_daemon/__init__.py",
    "content": ""
  },
  {
    "path": "tests/unit/lbrynet_daemon/test_Daemon.py",
    "content": "import unittest\nfrom unittest import mock\nimport json\n\nfrom lbry.extras.daemon.storage import SQLiteStorage\nfrom lbry.extras.daemon.componentmanager import ComponentManager\nfrom lbry.extras.daemon.components import DATABASE_COMPONENT, DHT_COMPONENT, WALLET_COMPONENT\nfrom lbry.extras.daemon.components import HASH_ANNOUNCER_COMPONENT\nfrom lbry.extras.daemon.components import UPNP_COMPONENT, BLOB_COMPONENT\nfrom lbry.extras.daemon.components import PEER_PROTOCOL_SERVER_COMPONENT, EXCHANGE_RATE_MANAGER_COMPONENT\nfrom lbry.extras.daemon.daemon import Daemon as LBRYDaemon\nfrom lbry.wallet import WalletManager, Wallet\nfrom lbry.conf import Config\n\nfrom tests import test_utils\n# from tests.mocks import mock_conf_settings, FakeNetwork, FakeFileManager\n# from tests.mocks import ExchangeRateManager as DummyExchangeRateManager\n# from tests.mocks import BTCLBCFeed, USDBTCFeed\nfrom tests.test_utils import is_android\n\n\ndef get_test_daemon(conf: Config, with_fee=False):\n    conf.data_dir = '/tmp'\n    rates = {\n        'BTCLBC': {'spot': 3.0, 'ts': test_utils.DEFAULT_ISO_TIME + 1},\n        'USDBTC': {'spot': 2.0, 'ts': test_utils.DEFAULT_ISO_TIME + 2}\n    }\n    component_manager = ComponentManager(\n        conf, skip_components=[\n            DATABASE_COMPONENT, DHT_COMPONENT, WALLET_COMPONENT, UPNP_COMPONENT,\n            PEER_PROTOCOL_SERVER_COMPONENT, HASH_ANNOUNCER_COMPONENT,\n            EXCHANGE_RATE_MANAGER_COMPONENT, BLOB_COMPONENT,\n            RATE_LIMITER_COMPONENT],\n        file_manager=FakeFileManager\n    )\n    daemon = LBRYDaemon(conf, component_manager=component_manager)\n    daemon.payment_rate_manager = OnlyFreePaymentsManager()\n    daemon.wallet_manager = mock.Mock(spec=WalletManager)\n    daemon.wallet_manager.wallet = mock.Mock(spec=Wallet)\n    daemon.wallet_manager.use_encryption = False\n    daemon.wallet_manager.network = FakeNetwork()\n    daemon.storage = mock.Mock(spec=SQLiteStorage)\n    market_feeds = [BTCLBCFeed(), USDBTCFeed()]\n    daemon.exchange_rate_manager = DummyExchangeRateManager(market_feeds, rates)\n    daemon.stream_manager = component_manager.get_component(FILE_MANAGER_COMPONENT)\n\n    metadata = {\n        \"author\": \"fake author\",\n        \"language\": \"en\",\n        \"content_type\": \"fake/format\",\n        \"description\": \"fake description\",\n        \"license\": \"fake license\",\n        \"license_url\": \"fake license url\",\n        \"nsfw\": False,\n        \"sources\": {\n            \"lbry_sd_hash\": 'd2b8b6e907dde95245fe6d144d16c2fdd60c4e0c6463ec98'\n                            'b85642d06d8e9414e8fcfdcb7cb13532ec5454fb8fe7f280'\n        },\n        \"thumbnail\": \"fake thumbnail\",\n        \"title\": \"fake title\",\n        \"ver\": \"0.0.3\"\n    }\n    if with_fee:\n        metadata.update(\n            {\"fee\": {\"USD\": {\"address\": \"bQ6BGboPV2SpTMEP7wLNiAcnsZiH8ye6eA\", \"amount\": 0.75}}})\n    migrated = smart_decode(json.dumps(metadata))\n    daemon._resolve = daemon.resolve = lambda *_: defer.succeed(\n        {\"test\": {'claim': {'value': migrated.claim_dict}}})\n    return daemon\n\n\n@unittest.SkipTest\nclass TestCostEst(unittest.TestCase):\n    def setUp(self):\n        test_utils.reset_time(self)\n\n    def test_fee_and_generous_data(self):\n        size = 10000000\n        correct_result = 4.5\n        daemon = get_test_daemon(Config(is_generous_host=True), with_fee=True)\n        result = yield f2d(daemon.get_est_cost(\"test\", size))\n        self.assertEqual(result, correct_result)\n\n    def test_generous_data_and_no_fee(self):\n        size = 10000000\n        correct_result = 0.0\n        daemon = get_test_daemon(Config(is_generous_host=True))\n        result = yield f2d(daemon.get_est_cost(\"test\", size))\n        self.assertEqual(result, correct_result)\n\n\n@unittest.SkipTest\nclass TestJsonRpc(unittest.TestCase):\n    def setUp(self):\n        async def noop():\n            return None\n\n        test_utils.reset_time(self)\n        self.test_daemon = get_test_daemon(Config())\n        self.test_daemon.wallet_manager.get_best_blockhash = noop\n\n    def test_status(self):\n        status = yield f2d(self.test_daemon.jsonrpc_status())\n        self.assertDictContainsSubset({'is_running': False}, status)\n\n    def test_help(self):\n        result = self.test_daemon.jsonrpc_help(command='status')\n        self.assertSubstring('daemon status', result['help'])\n\n    if is_android():\n        test_help.skip = \"Test cannot pass on Android because PYTHONOPTIMIZE removes the docstrings.\"\n\n\n@unittest.SkipTest\nclass TestFileListSorting(unittest.TestCase):\n    def setUp(self):\n        test_utils.reset_time(self)\n        self.test_daemon = get_test_daemon(Config())\n        self.test_daemon.file_manager.lbry_files = self._get_fake_lbry_files()\n\n        self.test_points_paid = [\n            2.5, 4.8, 5.9, 5.9, 5.9, 6.1, 7.1, 8.2, 8.4, 9.1\n        ]\n        self.test_file_names = [\n            'add.mp3', 'any.mov', 'day.tiff', 'decade.odt', 'different.json', 'hotel.bmp',\n            'might.bmp', 'physical.json', 'remember.mp3', 'than.ppt'\n        ]\n        self.test_authors = [\n            'ashlee27', 'bfrederick', 'brittanyhicks', 'davidsonjeffrey', 'heidiherring',\n            'jlewis', 'kswanson', 'michelle50', 'richard64', 'xsteele'\n        ]\n        return f2d(self.test_daemon.component_manager.start())\n\n    def test_sort_by_points_paid_no_direction_specified(self):\n        sort_options = ['points_paid']\n        file_list = yield f2d(self.test_daemon.jsonrpc_file_list(sort=sort_options)['items'])\n        self.assertEqual(self.test_points_paid, [f['points_paid'] for f in file_list])\n\n    def test_sort_by_points_paid_ascending(self):\n        sort_options = ['points_paid,asc']\n        file_list = yield f2d(self.test_daemon.jsonrpc_file_list(sort=sort_options)['items'])\n        self.assertEqual(self.test_points_paid, [f['points_paid'] for f in file_list])\n\n    def test_sort_by_points_paid_descending(self):\n        sort_options = ['points_paid, desc']\n        file_list = yield f2d(self.test_daemon.jsonrpc_file_list(sort=sort_options)['items'])\n        self.assertEqual(list(reversed(self.test_points_paid)), [f['points_paid'] for f in file_list])\n\n    def test_sort_by_file_name_no_direction_specified(self):\n        sort_options = ['file_name']\n        file_list = yield f2d(self.test_daemon.jsonrpc_file_list(sort=sort_options)['items'])\n        self.assertEqual(self.test_file_names, [f['file_name'] for f in file_list])\n\n    def test_sort_by_file_name_ascending(self):\n        sort_options = ['file_name,\\nasc']\n        file_list = yield f2d(self.test_daemon.jsonrpc_file_list(sort=sort_options)['items'])\n        self.assertEqual(self.test_file_names, [f['file_name'] for f in file_list])\n\n    def test_sort_by_file_name_descending(self):\n        sort_options = ['\\tfile_name,\\n\\tdesc']\n        file_list = yield f2d(self.test_daemon.jsonrpc_file_list(sort=sort_options)['items'])\n        self.assertEqual(list(reversed(self.test_file_names)), [f['file_name'] for f in file_list])\n\n    def test_sort_by_multiple_criteria(self):\n        expected = [\n            'file_name=different.json, points_paid=9.1',\n            'file_name=physical.json, points_paid=8.4',\n            'file_name=any.mov, points_paid=8.2',\n            'file_name=hotel.bmp, points_paid=7.1',\n            'file_name=add.mp3, points_paid=6.1',\n            'file_name=decade.odt, points_paid=5.9',\n            'file_name=might.bmp, points_paid=5.9',\n            'file_name=than.ppt, points_paid=5.9',\n            'file_name=remember.mp3, points_paid=4.8',\n            'file_name=day.tiff, points_paid=2.5'\n        ]\n        format_result = lambda f: f\"file_name={f['file_name']}, points_paid={f['points_paid']}\"\n\n        sort_options = ['file_name,asc', 'points_paid,desc']\n        file_list = yield f2d(self.test_daemon.jsonrpc_file_list(sort=sort_options)['items'])\n        self.assertEqual(expected, [format_result(r) for r in file_list])\n\n        # Check that the list is not sorted as expected when sorted only by file_name.\n        sort_options = ['file_name,asc']\n        file_list = yield f2d(self.test_daemon.jsonrpc_file_list(sort=sort_options)['items'])\n        self.assertNotEqual(expected, [format_result(r) for r in file_list])\n\n        # Check that the list is not sorted as expected when sorted only by points_paid.\n        sort_options = ['points_paid,desc']\n        file_list = yield f2d(self.test_daemon.jsonrpc_file_list(sort=sort_options)['items'])\n        self.assertNotEqual(expected, [format_result(r) for r in file_list])\n\n        # Check that the list is not sorted as expected when not sorted at all.\n        file_list = yield f2d(self.test_daemon.jsonrpc_file_list()['items'])\n        self.assertNotEqual(expected, [format_result(r) for r in file_list])\n\n    def test_sort_by_nested_field(self):\n        extract_authors = lambda file_list: [f['metadata']['author'] for f in file_list]\n\n        sort_options = ['metadata.author']\n        file_list = yield f2d(self.test_daemon.jsonrpc_file_list(sort=sort_options)['items'])\n        self.assertEqual(self.test_authors, extract_authors(file_list))\n\n        # Check that the list matches the expected in reverse when sorting in descending order.\n        sort_options = ['metadata.author,desc']\n        file_list = yield f2d(self.test_daemon.jsonrpc_file_list(sort=sort_options)['items'])\n        self.assertEqual(list(reversed(self.test_authors)), extract_authors(file_list))\n\n        # Check that the list is not sorted as expected when not sorted at all.\n        file_list = yield f2d(self.test_daemon.jsonrpc_file_list()['items'])\n        self.assertNotEqual(self.test_authors, extract_authors(file_list))\n\n    def test_invalid_sort_produces_meaningful_errors(self):\n        sort_options = ['meta.author']\n        expected_message = \"Failed to get 'meta.author', key 'meta' was not found.\"\n        with self.assertRaisesRegex(Exception, expected_message):\n            yield f2d(self.test_daemon.jsonrpc_file_list(sort=sort_options)['items'])\n        sort_options = ['metadata.foo.bar']\n        expected_message = \"Failed to get 'metadata.foo.bar', key 'foo' was not found.\"\n        with self.assertRaisesRegex(Exception, expected_message):\n            yield f2d(self.test_daemon.jsonrpc_file_list(sort=sort_options)['items'])\n\n    @staticmethod\n    def _get_fake_lbry_files():\n        faked_lbry_files = []\n        for metadata in FAKED_LBRY_FILES:\n            lbry_file = mock.Mock(spec=ManagedEncryptedFileDownloader)\n            for attribute in metadata:\n                setattr(lbry_file, attribute, metadata[attribute])\n            async def get_total_bytes():\n                return 0\n            lbry_file.get_total_bytes = get_total_bytes\n            async def status():\n                return EncryptedFileStatusReport(\n                    'file_name', 1, 1, 'completed'\n                )\n            lbry_file.status = status\n            faked_lbry_files.append(lbry_file)\n        return faked_lbry_files\n\n\nFAKED_LBRY_FILES = (\n    {\n        'channel_claim_id': '3aace03b007d108c668d201533b7b07ab2981d47',\n        'channel_name': '@ashlee27',\n        'claim_id': 'cb63e644b6629467c031d0097d52ab6e0f1a5bf8',\n        'claim_name': 'very-skill-place-growth',\n        'completed': True,\n        'download_directory': '/usually',\n        'download_path': '/usually/any.mov',\n        'file_name': 'any.mov',\n        'key': b'>a\\x11}\\xec\\xc2j\\x1c\\xe9\\xc5l]\\xfc\\x16s|',\n        'metadata': {'author': 'ashlee27', 'nsfw': True},\n        'mime_type': 'multipart/signed',\n        'nout': 7197,\n        'outpoint': 'c5633a5932f9c8e3e5b9799c07251b236e3aec078b0546614f24a932b6b133f6',\n        'points_paid': 8.2,\n        'sd_hash': '3354ecf502870f6f6d59d21188755c1361c2cffaeb458764c179c136b26c4795083acfd93b3920218870b1a9c22535ef',\n        'stopped': True,\n        'stream_hash': 'c8f58a686726116c15a8de7f33b4f0d72504777c7dd0c48ba94d7bbea23d9c82b2f977081cd7c49d25d6a2841b232e1d',\n        'stream_name': 'down.txt',\n        'suggested_file_name': 'down.txt',\n        'txid': '1c02986bfdb77b1c338e60b60c4c9febc59130af2e225f51665067c3d3419a35',\n        'written_bytes': 6838,\n    },\n    {\n        'channel_claim_id': 'dade35ea84001858d7cf10f50be3b5fea3e57fb7',\n        'channel_name': '@richard64',\n        'claim_id': '1c01096727da90140d333197fa8aaf88893f6ea8',\n        'claim_name': 'room-tonight-produce-good',\n        'completed': False,\n        'download_directory': '/ability',\n        'download_path': '/ability/day.tiff',\n        'file_name': 'day.tiff',\n        'key': b'`\\x86j\\xba\\x97\\x0c\\xe4L\\xad\\x06nC\\x8b]\\xd6&',\n        'metadata': {'author': 'richard64', 'nsfw': False},\n        'mime_type': 'multipart/related',\n        'nout': 9678,\n        'outpoint': '0138083012ce4ff5a6e4c0ec2fc08e11a52ebd2306d70a20f36424011f7c1330',\n        'points_paid': 2.5,\n        'sd_hash': '9e98f5e4bd4393b45a41839fe72a4df1f94b029e2339f8b14b9eaa9fec2be5245b160a59cfcc80fa86c1f91da67d5581',\n        'stopped': False,\n        'stream_hash': '7403ff9319292bdb022d66d0b88401775c6bb355fc0a75fe01452950fb19642ba58d492d34e24d8bf58375e6c2fca16f',\n        'stream_name': 'including.mp3',\n        'suggested_file_name': 'including.mp3',\n        'txid': '345bfc7b4a0c042b14474ac2cecf099236aec5a6730943630ffb176e7b421121',\n        'written_bytes': 8438,\n    },\n    {\n        'channel_claim_id': '59766071ea2df38b4a751834a77246bf8ff8071d',\n        'channel_name': '@bfrederick',\n        'claim_id': '1c08967d515ac2a5fd3cb4e477fde18bddde22e2',\n        'claim_name': 'at-first-skill-agency',\n        'completed': True,\n        'download_directory': '/agree',\n        'download_path': '/agree/different.json',\n        'file_name': 'different.json',\n        'key': b'\\xc06\\xb9\\x8e\\x00S\\xdbX\\x1cC\\xbd+\\xfc\\xea\\xc96',\n        'metadata': {'author': 'bfrederick', 'nsfw': True},\n        'mime_type': 'image/svg+xml',\n        'nout': 2975,\n        'outpoint': '62bb992e11c562e8064aec094e2b4eefb422e50b13ae49dc10f440a60f8e99bc',\n        'points_paid': 9.1,\n        'sd_hash': '3d809caf1266ec1ab78cc046a62f388434b6c59f85844500f59a1c75b4303b9ea27c532a231556e8b9776c544677bdf7',\n        'stopped': True,\n        'stream_hash': '9ecb8cf7dca7260f90666f05c88882017c786d31b572f3cba9447099ca9b49cdcb93f801db2249b7d32ff44ca6ffd69c',\n        'stream_name': 'north.doc',\n        'suggested_file_name': 'north.doc',\n        'txid': '98e12bce3a5db96e3513f1ff45afca8b69d61324117d6e839075ac512dd86251',\n        'written_bytes': 1929,\n    },\n    {\n        'channel_claim_id': '046c8a762cd158a6e5b112d7d9c9e4b778f27388',\n        'channel_name': '@heidiherring',\n        'claim_id': '3d7573264601af7b5402cd54a66c13f2f93f296f',\n        'claim_name': 'drop-hot-military-drive',\n        'completed': False,\n        'download_directory': '/letter',\n        'download_path': '/letter/than.ppt',\n        'file_name': 'than.ppt',\n        'key': b'\\n\\xa7\\xb3\\x05\\xab\\x8e\\xcc\\n\\xdcn\\xd9\\x81\\xf3m\\xf1t',\n        'metadata': {'author': 'heidiherring', 'nsfw': True},\n        'mime_type': 'text/javascript',\n        'nout': 3452,\n        'outpoint': '5e8b55ffe59774366804c384f632f728f769bad566c784c6a23f1081724d00ea',\n        'points_paid': 5.9,\n        'sd_hash': 'c945b0acaf1c97dd7f262cf73cc3813ab6552f695ab00a445ca07a2a4da43bf3bb020cc7e338b484405d0865ef836480',\n        'stopped': False,\n        'stream_hash': '692ea08506d13422c875ce9d49fb9fe90b828d259d46c53adffa68a045e3852d4763b90ef94ef3864a1164bcab7eefa0',\n        'stream_name': 'few.css',\n        'suggested_file_name': 'few.css',\n        'txid': '9ef901facbf8bc133cfe67793fe4423c048753dab8370d987f6f552ed6483bbb',\n        'written_bytes': 9498,\n    },\n    {\n        'channel_claim_id': 'adc87fa84d601aa1760d0f4585f02e60bc82c703',\n        'channel_name': '@xsteele',\n        'claim_id': '9022e1fd14646f6a4f6708566fbb6f6ac10ba3d5',\n        'claim_name': 'mean-television-miss-yourself',\n        'completed': True,\n        'download_directory': '/its',\n        'download_path': '/its/add.mp3',\n        'file_name': 'add.mp3',\n        'key': b'\\xdc\\xd1\\xf1i!\\x85\\xc6\\xc8\\\\\\xe0\\xd7\\xc0\\xceN<l',\n        'metadata': {'author': 'xsteele', 'nsfw': False},\n        'mime_type': 'message/imdn+xml',\n        'nout': 7924,\n        'outpoint': '4fd8a071fd00050006a666de076595d7a61e04dac0ce8bf9ef90024d2415ba30',\n        'points_paid': 6.1,\n        'sd_hash': 'ac28c50337bd16b4a753a2ae6bdad25cbb93270b83f593ec238b8237ce4ddf30aff676eb7025211d970ab5a5cca204c7',\n        'stopped': True,\n        'stream_hash': 'a0a9aa762fb6599f94e7098d70cc14ced34d08c210863d62b6d1e9c7eb523d98d0d8e3c80ddc03ce68b51f99e88024b5',\n        'stream_name': 'picture.csv',\n        'suggested_file_name': 'picture.csv',\n        'txid': 'b4df85f9be396f2d9a3b9172f826be9899608d827cabc5196863ec27d3a32f82',\n        'written_bytes': 9220,\n    },\n    {\n        'channel_claim_id': '79d17bbcb93b31c20fe395190dc199d871268ef1',\n        'channel_name': '@michelle50',\n        'claim_id': 'daf4c15cd3da305e7b29b0028cf801c61bd67e30',\n        'claim_name': 'card-oil-since-take',\n        'completed': True,\n        'download_directory': '/lawyer',\n        'download_path': '/lawyer/hotel.bmp',\n        'file_name': 'hotel.bmp',\n        'key': b'\\xda<-\\x11-\\xbb\\xe3u\\x80\\xffX\\x01N\\xfc\\x01)',\n        'metadata': {'author': 'michelle50', 'nsfw': True},\n        'mime_type': 'image/png',\n        'nout': 5576,\n        'outpoint': '27a62194fbf658327899431ecf251866bd0eec4da24d0b3feb8c440c4ea3ac1a',\n        'points_paid': 7.1,\n        'sd_hash': '20e4fc4513d7ea6f270e2021f7057c78e175754561e7db94e10c41d6d74ca639bb3c4d6e38dc2817ce303629c901d1a2',\n        'stopped': False,\n        'stream_hash': '781785e554fadb275ba75ee58cc5db0063f4d9cf2a1f1c4053a586ddce20089197d42e6640398cec75c8623a7c38ae0b',\n        'stream_name': 'paper.jpeg',\n        'suggested_file_name': 'paper.jpeg',\n        'txid': '8647c42f762694237804eeed4cbde776490a4f3b8b293ca41f550488098f883e',\n        'written_bytes': 7382,\n    },\n    {\n        'channel_claim_id': '7e4cee485713909665c21246ba22159e0a20a820',\n        'channel_name': '@jlewis',\n        'claim_id': '7bc402e4bc6a8b1c1aa2184e8e082eb1d0353db3',\n        'claim_name': 'heavy-street-meeting-and',\n        'completed': True,\n        'download_directory': '/personal',\n        'download_path': '/personal/remember.mp3',\n        'file_name': 'remember.mp3',\n        'key': b'\\x1c\\x9d%\\x1e\\xe4\\xab\\xb9\\x0c\\xac<\\x86\\xc7P;\\xfdO',\n        'metadata': {'author': 'jlewis', 'nsfw': True},\n        'mime_type': 'video/x-matroska',\n        'nout': 8116,\n        'outpoint': 'e2da970db0edb37680519a58de53dc088e6d26d5c4a37ae37d5c0f1901f30197',\n        'points_paid': 4.8,\n        'sd_hash': 'f42913f4bfba90f157b4744b55e4043d79d2d658dc08aa306b34a3ae4a1c1c37759fd9f0b4b8181f539bd60373746954',\n        'stopped': True,\n        'stream_hash': '4051a577422fe2b444c9c572a0a1b3f731e0ed2e5eb3b9a3aaa4ce1b0ec694cd7786e224c94a126fc9a868f1b93cb2e1',\n        'stream_name': 'feel.html',\n        'suggested_file_name': 'feel.html',\n        'txid': 'c88e0c86ebbca20d0dafed682711a0b2e02e80637515c25eb26f2a589385bfe2',\n        'written_bytes': 9337,\n    },\n    {\n        'channel_claim_id': '13aa3c28c0c8bb08a679a010f63bd3f4b5234e73',\n        'channel_name': '@brittanyhicks',\n        'claim_id': '3c9c02bf1bfcedb2654f9003c464df9059a8e6b0',\n        'claim_name': 'cold-music-admit-technology',\n        'completed': True,\n        'download_directory': '/nor',\n        'download_path': '/nor/might.bmp',\n        'file_name': 'might.bmp',\n        'key': b'\\rh\\xb3jqR\\\\\\xdb\\xb9\\xa0a\\xa4J\\xa4\\xacs',\n        'metadata': {'author': 'brittanyhicks', 'nsfw': True},\n        'mime_type': 'application/xop+xml',\n        'nout': 8338,\n        'outpoint': 'a68a9dfa301292e1d0fe60a9bb0bcefa3e4e26630269064c8d2dd0f578427a10',\n        'points_paid': 5.9,\n        'sd_hash': '48fef5178b93b542495d19d76407692802ab529d989539b203a1cb38ce35ec2d4e9ea7d31eac660f715c39b69cd574ec',\n        'stopped': True,\n        'stream_hash': '2052889a9447ea73d743ec2c8c71678bf60616c01cd05d0a4d34a1aa92ee334585771a28f42cfe1b4124645352325946',\n        'stream_name': 'shoulder.js',\n        'suggested_file_name': 'shoulder.js',\n        'txid': 'b3f5f9db4c40157f348b9cf7dcb4ae3c53fe5e43481a4b66b2cc2334ae5ad2cb',\n        'written_bytes': 9736,\n    },\n    {\n        'channel_claim_id': 'a18b45f2131fd79fea6bb493d94349c9734ef211',\n        'channel_name': '@kswanson',\n        'claim_id': '1223727010f0b4b9f6f45ca95cad0bfb3ce759a0',\n        'claim_name': 'often-speech-provide-run',\n        'completed': True,\n        'download_directory': '/member',\n        'download_path': '/member/physical.json',\n        'file_name': 'physical.json',\n        'key': b\"'\\xa7\\x9b!\\t\\x86\\xe2q\\x15S\\x9c\\x92S@7;\",\n        'metadata': {'author': 'kswanson', 'nsfw': True},\n        'mime_type': 'multipart/form-data',\n        'nout': 7028,\n        'outpoint': '818a4265723d7682cff4cc89d9b3433af48636ba42d2ca1e65eef8b7f9bef0ad',\n        'points_paid': 8.4,\n        'sd_hash': 'c808d997ff914c4986e420c4b2547ab030082da28ffebe2a0844ad6325c9f276fad5a003b18dcd015397e41b71d172e2',\n        'stopped': False,\n        'stream_hash': '2d457dda5ed01009b3812ff60bd24cbc2a0cb1361f566433d71dbce7d757977deac7f5aca62a60ec63eaa1b401194da5',\n        'stream_name': 'country.avi',\n        'suggested_file_name': 'country.avi',\n        'txid': '42db2b952c578afcb8f640c2a12e563ba1a31b18fc8357d2f04f5de6c8515fba',\n        'written_bytes': 9688,\n    },\n    {\n        'channel_claim_id': '84a06ce77cde8ed1511e268fcfdebd8feb1333e2',\n        'channel_name': '@davidsonjeffrey',\n        'claim_id': '8fb403f0bb0695530935a0991a7eb7218c46eed9',\n        'claim_name': 'option-company-glass-this',\n        'completed': True,\n        'download_directory': '/environment',\n        'download_path': '/environment/decade.odt',\n        'file_name': 'decade.odt',\n        'key': b')\\xa3h\\x12\\xf2\\xd5RPkWojN\\x08%\\x0e',\n        'metadata': {'author': 'davidsonjeffrey', 'nsfw': True},\n        'mime_type': 'video/webm',\n        'nout': 8810,\n        'outpoint': '04da67fe9c6d129812e16045c02f1f670d3e329e7a9c0872712aaa74876becdd',\n        'points_paid': 5.9,\n        'sd_hash': '394eb1e0caf0d7dbeb80d435631534dc716229fac035aebe2af1729af5cbbad1c4fa503ce7fa7cc01e5366d1ce9d9d07',\n        'stopped': False,\n        'stream_hash': '1904d27ab8c784b7ae770980f004522e36089e86e3ce95d3005c3829cf4ad1571c5fe248a3f67a54521e07290a9e7466',\n        'stream_name': 'score.wav',\n        'suggested_file_name': 'score.wav',\n        'txid': 'd2f8ecfac4491e1de186b43a5e561413304769a1683612a16633dd3e725ff1e0',\n        'written_bytes': 7929,\n    },\n)\n"
  },
  {
    "path": "tests/unit/lbrynet_daemon/test_allowed_origin.py",
    "content": "import unittest\n\nfrom aiohttp import ClientSession\nfrom aiohttp.test_utils import make_mocked_request as request\nfrom aiohttp.web import HTTPForbidden\n\nfrom lbry.testcase import AsyncioTestCase\nfrom lbry.conf import Config\nfrom lbry.extras.daemon.security import is_request_allowed as allowed, ensure_request_allowed as ensure\nfrom lbry.extras.daemon.components import (\n    DATABASE_COMPONENT, DISK_SPACE_COMPONENT, BLOB_COMPONENT, WALLET_COMPONENT, DHT_COMPONENT,\n    HASH_ANNOUNCER_COMPONENT, FILE_MANAGER_COMPONENT, PEER_PROTOCOL_SERVER_COMPONENT,\n    UPNP_COMPONENT, EXCHANGE_RATE_MANAGER_COMPONENT, WALLET_SERVER_PAYMENTS_COMPONENT,\n    LIBTORRENT_COMPONENT, BACKGROUND_DOWNLOADER_COMPONENT, TRACKER_ANNOUNCER_COMPONENT\n)\nfrom lbry.extras.daemon.daemon import Daemon\n\n\nclass TestAllowedOrigin(unittest.TestCase):\n\n    def test_allowed_origin_default(self):\n        conf = Config()\n        # lack of Origin is always allowed\n        self.assertTrue(allowed(request('GET', '/'), conf))\n        # deny all other Origins\n        self.assertFalse(allowed(request('GET', '/', headers={'Origin': 'null'}), conf))\n        self.assertFalse(allowed(request('GET', '/', headers={'Origin': 'localhost'}), conf))\n        self.assertFalse(allowed(request('GET', '/', headers={'Origin': 'hackers.com'}), conf))\n\n    def test_allowed_origin_star(self):\n        conf = Config(allowed_origin='*')\n        # everything is allowed\n        self.assertTrue(allowed(request('GET', '/'), conf))\n        self.assertTrue(allowed(request('GET', '/', headers={'Origin': 'null'}), conf))\n        self.assertTrue(allowed(request('GET', '/', headers={'Origin': 'localhost'}), conf))\n        self.assertTrue(allowed(request('GET', '/', headers={'Origin': 'hackers.com'}), conf))\n\n    def test_allowed_origin_specified(self):\n        conf = Config(allowed_origin='localhost')\n        # no origin and only localhost are allowed\n        self.assertTrue(allowed(request('GET', '/'), conf))\n        self.assertTrue(allowed(request('GET', '/', headers={'Origin': 'localhost'}), conf))\n        self.assertFalse(allowed(request('GET', '/', headers={'Origin': 'null'}), conf))\n        self.assertFalse(allowed(request('GET', '/', headers={'Origin': 'hackers.com'}), conf))\n\n    def test_ensure_default(self):\n        conf = Config()\n        ensure(request('GET', '/'), conf)\n        with self.assertLogs() as log:\n            with self.assertRaises(HTTPForbidden):\n                ensure(request('GET', '/', headers={'Origin': 'localhost'}), conf)\n            self.assertIn(\"'localhost' are not allowed\", log.output[0])\n\n    def test_ensure_specific(self):\n        conf = Config(allowed_origin='localhost')\n        ensure(request('GET', '/', headers={'Origin': 'localhost'}), conf)\n        with self.assertLogs() as log:\n            with self.assertRaises(HTTPForbidden):\n                ensure(request('GET', '/', headers={'Origin': 'hackers.com'}), conf)\n            self.assertIn(\"'hackers.com' are not allowed\", log.output[0])\n            self.assertIn(\"'allowed_origin' limits requests to: 'localhost'\", log.output[0])\n\n\nclass TestAccessHeaders(AsyncioTestCase):\n\n    async def asyncSetUp(self):\n        conf = Config(allowed_origin='localhost')\n        conf.data_dir = '/tmp'\n        conf.share_usage_data = False\n        conf.api = 'localhost:5299'\n        conf.components_to_skip = (\n            DATABASE_COMPONENT, DISK_SPACE_COMPONENT, BLOB_COMPONENT, WALLET_COMPONENT, DHT_COMPONENT,\n            HASH_ANNOUNCER_COMPONENT, FILE_MANAGER_COMPONENT, PEER_PROTOCOL_SERVER_COMPONENT,\n            UPNP_COMPONENT, EXCHANGE_RATE_MANAGER_COMPONENT, WALLET_SERVER_PAYMENTS_COMPONENT,\n            LIBTORRENT_COMPONENT, BACKGROUND_DOWNLOADER_COMPONENT, TRACKER_ANNOUNCER_COMPONENT\n        )\n        Daemon.component_attributes = {}\n        self.daemon = Daemon(conf)\n        await self.daemon.start()\n        self.addCleanup(self.daemon.stop)\n\n    async def test_headers(self):\n        async with ClientSession() as session:\n\n            # OPTIONS\n            async with session.options('http://localhost:5299') as resp:\n                self.assertEqual(resp.headers['Access-Control-Allow-Origin'], 'localhost')\n                self.assertEqual(resp.headers['Access-Control-Allow-Methods'], 'localhost')\n                self.assertEqual(resp.headers['Access-Control-Allow-Headers'], 'localhost')\n\n            # GET\n            status = {'method': 'status', 'params': []}\n            async with session.get('http://localhost:5299/lbryapi', json=status) as resp:\n                self.assertEqual(resp.headers['Access-Control-Allow-Origin'], 'localhost')\n                self.assertEqual(resp.headers['Access-Control-Allow-Methods'], 'localhost')\n                self.assertEqual(resp.headers['Access-Control-Allow-Headers'], 'localhost')\n"
  },
  {
    "path": "tests/unit/lbrynet_daemon/test_exchange_rate_manager.py",
    "content": "import asyncio\nfrom decimal import Decimal\nfrom time import time\nfrom lbry.schema.claim import Claim\nfrom lbry.extras.daemon.exchange_rate_manager import (\n    ExchangeRate, ExchangeRateManager, CurrencyConversionError,\n    BittrexUSDFeed, BittrexBTCFeed,\n    CoinExBTCFeed\n)\nfrom lbry.testcase import AsyncioTestCase, FakeExchangeRateManager, get_fake_exchange_rate_manager\nfrom lbry.error import InvalidExchangeRateResponseError\n\n\nclass ExchangeRateTests(AsyncioTestCase):\n\n    def test_invalid_rates(self):\n        with self.assertRaises(ValueError):\n            ExchangeRate('USDBTC', 0, time())\n        with self.assertRaises(ValueError):\n            ExchangeRate('USDBTC', -1, time())\n\n    def test_fee_converts_to_lbc(self):\n        fee = Claim().stream.fee\n        fee.usd = Decimal(10.0)\n        fee.address = \"bRcHraa8bYJZL7vkh5sNmGwPDERFUjGPP9\"\n        manager = get_fake_exchange_rate_manager()\n        result = manager.convert_currency(fee.currency, \"LBC\", fee.amount)\n        self.assertEqual(20.0, result)\n\n    def test_missing_feed(self):\n        fee = Claim().stream.fee\n        fee.usd = Decimal(1.0)\n        fee.address = \"bRcHraa8bYJZL7vkh5sNmGwPDERFUjGPP9\"\n        manager = FakeExchangeRateManager([BittrexBTCFeed()], {'BTCLBC': 1.0})\n        with self.assertRaises(CurrencyConversionError):\n            manager.convert_currency(fee.currency, \"LBC\", fee.amount)\n\n    def test_bittrex_feed_response(self):\n        feed = BittrexBTCFeed()\n        out = feed.get_rate_from_response({\n            \"symbol\": \"LBC-BTC\",\n            \"lastTradeRate\": \"0.00000323\",\n            \"bidRate\": \"0.00000322\",\n            \"askRate\": \"0.00000327\"\n        })\n        self.assertEqual(1.0 / 0.00000323, out)\n        with self.assertRaises(InvalidExchangeRateResponseError):\n            feed.get_rate_from_response({})\n        with self.assertRaises(InvalidExchangeRateResponseError):\n            feed.get_rate_from_response({\n                \"success\": True,\n                \"result\": []\n            })\n\n\nclass BadMarketFeed(BittrexUSDFeed):\n\n    def get_response(self):\n        raise InvalidExchangeRateResponseError(self.name, 'bad stuff')\n\n\nclass ExchangeRateManagerTests(AsyncioTestCase):\n\n    async def test_get_rate_failure_retrieved(self):\n        manager = ExchangeRateManager([BadMarketFeed])\n        manager.start()\n        await manager.wait()\n        for feed in manager.market_feeds:  # no rate but it tried\n            self.assertFalse(feed.has_rate)\n            self.assertTrue(feed.event.is_set())\n        self.addCleanup(manager.stop)\n\n    async def test_median_rate_used(self):\n        manager = ExchangeRateManager([BittrexBTCFeed, CoinExBTCFeed])\n        for feed in manager.market_feeds:\n            feed.last_check = time()\n        bittrex, coinex = manager.market_feeds\n        bittrex.rate = ExchangeRate(bittrex.market, 1.0, time())\n        coinex.rate = ExchangeRate(coinex.market, 2.0, time())\n        coinex.rate = ExchangeRate(coinex.market, 3.0, time())\n        self.assertEqual(14.0, manager.convert_currency(\"BTC\", \"LBC\", Decimal(7.0)))\n        coinex.rate.spot = 4.0\n        self.assertEqual(17.5, manager.convert_currency(\"BTC\", \"LBC\", Decimal(7.0)))\n"
  },
  {
    "path": "tests/unit/lbrynet_daemon/test_mime_types.py",
    "content": "import unittest\nfrom lbry.schema import mime_types\n\n\nclass TestMimeTypes(unittest.TestCase):\n    def test_mp4_video(self):\n        self.assertEqual(\"video/mp4\", mime_types.guess_media_type(\"test.mp4\")[0])\n        self.assertEqual(\"video/mp4\", mime_types.guess_media_type(\"test.MP4\")[0])\n\n    def test_x_ext_(self):\n        self.assertEqual(\"application/x-ext-lbry\", mime_types.guess_media_type(\"test.lbry\")[0])\n        self.assertEqual(\"application/x-ext-lbry\", mime_types.guess_media_type(\"test.LBRY\")[0])\n\n    def test_octet_stream(self):\n        self.assertEqual(\"application/octet-stream\", mime_types.guess_media_type(\"test.\")[0])\n        self.assertEqual(\"application/octet-stream\", mime_types.guess_media_type(\"test\")[0])\n"
  },
  {
    "path": "tests/unit/schema/__init__.py",
    "content": ""
  },
  {
    "path": "tests/unit/schema/test_claim_from_bytes.py",
    "content": "from unittest import TestCase\nfrom binascii import unhexlify\n\nfrom lbry.schema import Claim\n\n\nclass TestOldJSONSchemaCompatibility(TestCase):\n\n    def test_old_json_schema_v1(self):\n        claim = Claim.from_bytes(\n            b'{\"fee\": {\"LBC\": {\"amount\": 1.0, \"address\": \"bPwGA9h7uijoy5uAvzVPQw9QyLoYZehHJo\"}}, \"d'\n            b'escription\": \"10MB test file to measure download speed on Lbry p2p-network.\", \"licens'\n            b'e\": \"None\", \"author\": \"root\", \"language\": \"English\", \"title\": \"10MB speed test file\",'\n            b' \"sources\": {\"lbry_sd_hash\": \"bbd1f68374ff9a1044a90d7dd578ce41979211c386caf19e6f49653'\n            b'6db5f2c96b58fe2c7a6677b331419a117873b539f\"}, \"content-type\": \"application/octet-strea'\n            b'm\", \"thumbnail\": \"/home/robert/lbry/speed.jpg\"}'\n        )\n        stream = claim.stream\n        self.assertEqual(stream.title, '10MB speed test file')\n        self.assertEqual(stream.description, '10MB test file to measure download speed on Lbry p2p-network.')\n        self.assertEqual(stream.license, 'None')\n        self.assertEqual(stream.author, 'root')\n        self.assertEqual(stream.langtags, ['en'])\n        self.assertEqual(stream.source.media_type, 'application/octet-stream')\n        self.assertEqual(stream.thumbnail.url, '/home/robert/lbry/speed.jpg')\n        self.assertEqual(\n            stream.source.sd_hash,\n            'bbd1f68374ff9a1044a90d7dd578ce41979211c386caf19e'\n            '6f496536db5f2c96b58fe2c7a6677b331419a117873b539f'\n        )\n        self.assertEqual(stream.fee.address, 'bPwGA9h7uijoy5uAvzVPQw9QyLoYZehHJo')\n        self.assertEqual(stream.fee.lbc, 1)\n        self.assertEqual(stream.fee.dewies, 100000000)\n        self.assertEqual(stream.fee.currency, 'LBC')\n        with self.assertRaisesRegex(ValueError, 'USD can only be returned for USD fees.'):\n            print(stream.fee.usd)\n\n    def test_old_json_schema_v2(self):\n        claim = Claim.from_bytes(\n            b'{\"license\": \"Creative Commons Attribution 3.0 United States\", \"fee\": {\"LBC\": {\"amount'\n            b'\": 10, \"address\": \"bFro33qBKxnL1AsjUU9N4AQHp9V62Nhc5L\"}}, \"ver\": \"0.0.2\", \"descriptio'\n            b'n\": \"Force P0 State for Nividia Cards! (max mining performance)\", \"language\": \"en\", \"'\n            b'author\": \"Mii\", \"title\": \"Nividia P0\", \"sources\": {\"lbry_sd_hash\": \"c5ffee0fa5168e166'\n            b'81b519d9d85446e8d1d818a616bd55540aa7374d2321b51abf2ac3dae1443a03dadcc8f7affaa62\"}, \"n'\n            b'sfw\": false, \"license_url\": \"https://creativecommons.org/licenses/by/3.0/us/legalcode'\n            b'\", \"content-type\": \"application/x-msdownload\"}'\n        )\n        stream = claim.stream\n        self.assertEqual(stream.title, 'Nividia P0')\n        self.assertEqual(stream.description, 'Force P0 State for Nividia Cards! (max mining performance)')\n        self.assertEqual(stream.license, 'Creative Commons Attribution 3.0 United States')\n        self.assertEqual(stream.license_url, 'https://creativecommons.org/licenses/by/3.0/us/legalcode')\n        self.assertEqual(stream.author, 'Mii')\n        self.assertEqual(stream.langtags, ['en'])\n        self.assertEqual(stream.source.media_type, 'application/x-msdownload')\n        self.assertEqual(\n            stream.source.sd_hash,\n            'c5ffee0fa5168e16681b519d9d85446e8d1d818a616bd555'\n            '40aa7374d2321b51abf2ac3dae1443a03dadcc8f7affaa62'\n        )\n        self.assertEqual(stream.fee.address, 'bFro33qBKxnL1AsjUU9N4AQHp9V62Nhc5L')\n        self.assertEqual(stream.fee.lbc, 10)\n        self.assertEqual(stream.fee.dewies, 1000000000)\n        self.assertEqual(stream.fee.currency, 'LBC')\n        with self.assertRaisesRegex(ValueError, 'USD can only be returned for USD fees.'):\n            print(stream.fee.usd)\n\n    def test_old_json_schema_v3(self):\n        claim = Claim.from_bytes(\n            b'{\"ver\": \"0.0.3\", \"description\": \"asd\", \"license\": \"Creative Commons Attribution 4.0 I'\n            b'nternational\", \"author\": \"sgb\", \"title\": \"ads\", \"language\": \"en\", \"sources\": {\"lbry_s'\n            b'd_hash\": \"d83db664c6d7d570aa824300f4869e0bfb560e765efa477aebf566467f8d3a57f4f8c704cab'\n            b'1308eb75ff8b7e84e3caf\"}, \"content_type\": \"video/mp4\", \"nsfw\": false}'\n        )\n        stream = claim.stream\n        self.assertEqual(stream.title, 'ads')\n        self.assertEqual(stream.description, 'asd')\n        self.assertEqual(stream.license, 'Creative Commons Attribution 4.0 International')\n        self.assertEqual(stream.author, 'sgb')\n        self.assertEqual(stream.langtags, ['en'])\n        self.assertEqual(stream.source.media_type, 'video/mp4')\n        self.assertEqual(\n            stream.source.sd_hash,\n            'd83db664c6d7d570aa824300f4869e0bfb560e765efa477a'\n            'ebf566467f8d3a57f4f8c704cab1308eb75ff8b7e84e3caf'\n        )\n\n\nclass TestTypesV1Compatibility(TestCase):\n\n    def test_signed_claim_made_by_ytsync(self):\n        claim = Claim.from_bytes(unhexlify(\n            b'080110011aee04080112a604080410011a2b4865726520617265203520526561736f6e73204920e29da4e'\n            b'fb88f204e657874636c6f7564207c20544c4722920346696e64206f7574206d6f72652061626f7574204e'\n            b'657874636c6f75643a2068747470733a2f2f6e657874636c6f75642e636f6d2f0a0a596f752063616e206'\n            b'6696e64206d65206f6e20746865736520736f6369616c733a0a202a20466f72756d733a2068747470733a'\n            b'2f2f666f72756d2e6865617679656c656d656e742e696f2f0a202a20506f64636173743a2068747470733'\n            b'a2f2f6f6666746f706963616c2e6e65740a202a2050617472656f6e3a2068747470733a2f2f7061747265'\n            b'6f6e2e636f6d2f7468656c696e757867616d65720a202a204d657263683a2068747470733a2f2f7465657'\n            b'37072696e672e636f6d2f73746f7265732f6f6666696369616c2d6c696e75782d67616d65720a202a2054'\n            b'77697463683a2068747470733a2f2f7477697463682e74762f786f6e64616b0a202a20547769747465723'\n            b'a2068747470733a2f2f747769747465722e636f6d2f7468656c696e757867616d65720a0a2e2e2e0a6874'\n            b'7470733a2f2f7777772e796f75747562652e636f6d2f77617463683f763d4672546442434f535f66632a0'\n            b'f546865204c696e75782047616d6572321c436f7079726967687465642028636f6e746163742061757468'\n            b'6f722938004a2968747470733a2f2f6265726b2e6e696e6a612f7468756d626e61696c732f46725464424'\n            b'34f535f666352005a001a41080110011a30040e8ac6e89c061f982528c23ad33829fd7146435bf7a4cc22'\n            b'f0bff70c4fe0b91fd36da9a375e3e1c171db825bf5d1f32209766964656f2f6d70342a5c080110031a406'\n            b'2b2dd4c45e364030fbfad1a6fefff695ebf20ea33a5381b947753e2a0ca359989a5cc7d15e5392a0d354c'\n            b'0b68498382b2701b22c03beb8dcb91089031b871e72214feb61536c007cdf4faeeaab4876cb397feaf6b51'\n        ))\n        stream = claim.stream\n        self.assertEqual(stream.title, 'Here are 5 Reasons I ❤️ Nextcloud | TLG')\n        self.assertEqual(\n            stream.description,\n            'Find out more about Nextcloud: https://nextcloud.com/\\n\\nYou can find me on these soci'\n            'als:\\n * Forums: https://forum.heavyelement.io/\\n * Podcast: https://offtopical.net\\n '\n            '* Patreon: https://patreon.com/thelinuxgamer\\n * Merch: https://teespring.com/stores/o'\n            'fficial-linux-gamer\\n * Twitch: https://twitch.tv/xondak\\n * Twitter: https://twitter.'\n            'com/thelinuxgamer\\n\\n...\\nhttps://www.youtube.com/watch?v=FrTdBCOS_fc'\n        )\n        self.assertEqual(stream.license, 'Copyrighted (contact author)')\n        self.assertEqual(stream.author, 'The Linux Gamer')\n        self.assertEqual(stream.langtags, ['en'])\n        self.assertEqual(stream.source.media_type, 'video/mp4')\n        self.assertEqual(stream.thumbnail.url, 'https://berk.ninja/thumbnails/FrTdBCOS_fc')\n        self.assertEqual(\n            stream.source.sd_hash,\n            '040e8ac6e89c061f982528c23ad33829fd7146435bf7a4cc'\n            '22f0bff70c4fe0b91fd36da9a375e3e1c171db825bf5d1f3'\n        )\n\n        # certificate for above channel\n        cert = Claim.from_bytes(unhexlify(\n            b'08011002225e0801100322583056301006072a8648ce3d020106052b8104000a034200043878b1edd4a13'\n            b'73149909ef03f4339f6da9c2bd2214c040fd2e530463ffe66098eca14fc70b50ff3aefd106049a815f595'\n            b'ed5a13eda7419ad78d9ed7ae473f17'\n        ))\n        channel = cert.channel\n        self.assertEqual(\n            channel.public_key,\n            '033878b1edd4a1373149909ef03f4339f6da9c2bd2214c040fd2e530463ffe6609'\n        )\n\n    def test_unsigned_with_fee(self):\n        claim = Claim.from_bytes(unhexlify(\n            b'080110011ad6010801127c080410011a08727067206d69646922046d6964692a08727067206d696469322'\n            b'e437265617469766520436f6d6d6f6e73204174747269627574696f6e20342e3020496e7465726e617469'\n            b'6f6e616c38004224080110011a19553f00bc139bbf40de425f94d51fffb34c1bea6d9171cd374c2500007'\n            b'0414a0052005a001a54080110011a301f41eb0312aa7e8a5ce49349bc77d811da975833719d751523b19f'\n            b'123fc3d528d6a94e3446ccddb7b9329f27a9cad7e3221c6170706c69636174696f6e2f782d7a69702d636'\n            b'f6d70726573736564'\n        ))\n        stream = claim.stream\n        self.assertEqual(stream.title, 'rpg midi')\n        self.assertEqual(stream.description, 'midi')\n        self.assertEqual(stream.license, 'Creative Commons Attribution 4.0 International')\n        self.assertEqual(stream.author, 'rpg midi')\n        self.assertEqual(stream.langtags, ['en'])\n        self.assertEqual(stream.source.media_type, 'application/x-zip-compressed')\n        self.assertEqual(\n            stream.source.sd_hash,\n            '1f41eb0312aa7e8a5ce49349bc77d811da975833719d7515'\n            '23b19f123fc3d528d6a94e3446ccddb7b9329f27a9cad7e3'\n        )\n        self.assertEqual(stream.fee.address, 'bJUQ9MxS9N6M29zsA5GTpVSDzsnPjMBBX9')\n        self.assertEqual(stream.fee.lbc, 15)\n        self.assertEqual(stream.fee.dewies, 1500000000)\n        self.assertEqual(stream.fee.currency, 'LBC')\n        with self.assertRaisesRegex(ValueError, 'USD can only be returned for USD fees.'):\n            print(stream.fee.usd)\n"
  },
  {
    "path": "tests/unit/schema/test_mime_types.py",
    "content": "import unittest\nimport tempfile\nimport os\n\nfrom lbry.schema.mime_types import guess_media_type\n\nclass MediaTypeTests(unittest.TestCase):\n    def test_guess_media_type_from_path_only(self):\n        kind = guess_media_type('/tmp/test.mkv')\n        self.assertEqual(kind, ('video/x-matroska', 'video'))\n\n    def test_defaults_for_no_extension(self):\n        kind = guess_media_type('/tmp/test')\n        self.assertEqual(kind, ('application/octet-stream', 'binary'))\n\n    def test_defaults_for_unknown_extension(self):\n        kind = guess_media_type('/tmp/test.unk')\n        self.assertEqual(kind, ('application/x-ext-unk', 'binary'))\n\n    def test_spoofed_unknown(self):\n        with tempfile.TemporaryDirectory() as temp_dir:\n            file = os.path.join(temp_dir, 'spoofed_unknown.txt')\n            with open(file, 'wb') as fd:\n                bytes_lz4 = bytearray([0x04,0x22,0x4d,0x18])\n                fd.write(bytes_lz4)\n                fd.close()\n\n            kind = guess_media_type(file)\n            self.assertEqual(kind, ('application/x-ext-lz4', 'binary'))\n\n    def test_spoofed_known(self):\n        with tempfile.TemporaryDirectory() as temp_dir:\n            file = os.path.join(temp_dir, 'spoofed_known.avi')\n            with open(file, 'wb') as fd:\n                bytes_zip = bytearray([0x50,0x4b,0x03,0x06])\n                fd.write(bytes_zip)\n                fd.close()\n\n            kind = guess_media_type(file)\n            self.assertEqual(kind, ('application/zip', 'binary'))\n\n    def test_spoofed_synonym(self):\n        with tempfile.TemporaryDirectory() as temp_dir:\n            file = os.path.join(temp_dir, 'spoofed_known.cbz')\n            with open(file, 'wb') as fd:\n                bytes_zip = bytearray([0x50,0x4b,0x03,0x06])\n                fd.write(bytes_zip)\n                fd.close()\n\n            kind = guess_media_type(file)\n            self.assertEqual(kind, ('application/vnd.comicbook+zip', 'document'))\n"
  },
  {
    "path": "tests/unit/schema/test_models.py",
    "content": "from unittest import TestCase\nfrom decimal import Decimal\n\nfrom lbry.schema.claim import Claim, Stream, Collection\nfrom lbry.error import InputValueIsNoneError\n\n\nclass TestClaimContainerAwareness(TestCase):\n\n    def test_stream_claim(self):\n        stream = Stream()\n        claim = stream.claim\n        self.assertEqual(claim.claim_type, Claim.STREAM)\n        claim = Claim.from_bytes(claim.to_bytes())\n        self.assertEqual(claim.claim_type, Claim.STREAM)\n        self.assertIsNotNone(claim.stream)\n        with self.assertRaisesRegex(ValueError, 'Claim is not a channel.'):\n            print(claim.channel)\n\n\nclass TestFee(TestCase):\n\n    def test_amount_setters(self):\n        stream = Stream()\n\n        stream.fee.lbc = Decimal('1.01')\n        self.assertEqual(stream.fee.lbc, Decimal('1.01'))\n        self.assertEqual(stream.fee.dewies, 101000000)\n        self.assertEqual(stream.fee.currency, 'LBC')\n        stream.fee.dewies = 203000000\n        self.assertEqual(stream.fee.lbc, Decimal('2.03'))\n        self.assertEqual(stream.fee.dewies, 203000000)\n        self.assertEqual(stream.fee.currency, 'LBC')\n        with self.assertRaisesRegex(ValueError, 'USD can only be returned for USD fees.'):\n            print(stream.fee.usd)\n        with self.assertRaisesRegex(ValueError, 'Pennies can only be returned for USD fees.'):\n            print(stream.fee.pennies)\n\n        stream.fee.usd = Decimal('1.01')\n        self.assertEqual(stream.fee.usd, Decimal('1.01'))\n        self.assertEqual(stream.fee.pennies, 101)\n        self.assertEqual(stream.fee.currency, 'USD')\n        stream.fee.pennies = 203\n        self.assertEqual(stream.fee.usd, Decimal('2.03'))\n        self.assertEqual(stream.fee.pennies, 203)\n        self.assertEqual(stream.fee.currency, 'USD')\n        with self.assertRaisesRegex(ValueError, 'LBC can only be returned for LBC fees.'):\n            print(stream.fee.lbc)\n        with self.assertRaisesRegex(ValueError, 'Dewies can only be returned for LBC fees.'):\n            print(stream.fee.dewies)\n\n\nclass TestLanguages(TestCase):\n\n    def test_language_successful_parsing(self):\n        stream = Stream()\n\n        stream.languages.append('en')\n        self.assertEqual(stream.languages[0].langtag, 'en')\n        self.assertEqual(stream.languages[0].language, 'en')\n        self.assertEqual(stream.langtags, ['en'])\n\n        stream.languages.append('en-US')\n        self.assertEqual(stream.languages[1].langtag, 'en-US')\n        self.assertEqual(stream.languages[1].language, 'en')\n        self.assertEqual(stream.languages[1].region, 'US')\n        self.assertEqual(stream.langtags, ['en', 'en-US'])\n\n        stream.languages.append('en-Latn-US')\n        self.assertEqual(stream.languages[2].langtag, 'en-Latn-US')\n        self.assertEqual(stream.languages[2].language, 'en')\n        self.assertEqual(stream.languages[2].script, 'Latn')\n        self.assertEqual(stream.languages[2].region, 'US')\n        self.assertEqual(stream.langtags, ['en', 'en-US', 'en-Latn-US'])\n\n        stream.languages.append('es-419')\n        self.assertEqual(stream.languages[3].langtag, 'es-419')\n        self.assertEqual(stream.languages[3].language, 'es')\n        self.assertIsNone(stream.languages[3].script)\n        self.assertEqual(stream.languages[3].region, '419')\n        self.assertEqual(stream.langtags, ['en', 'en-US', 'en-Latn-US', 'es-419'])\n\n        stream = Stream()\n        stream.languages.extend(['en-Latn-US', 'es-ES', 'de-DE'])\n        self.assertEqual(stream.languages[0].language, 'en')\n        self.assertEqual(stream.languages[1].language, 'es')\n        self.assertEqual(stream.languages[2].language, 'de')\n\n    def test_language_error_parsing(self):\n        stream = Stream()\n        with self.assertRaisesRegex(ValueError, \"Enum Language has no value defined for name 'zz'\"):\n            stream.languages.append('zz')\n        with self.assertRaisesRegex(ValueError, \"Enum Script has no value defined for name 'Zabc'\"):\n            stream.languages.append('en-Zabc')\n        with self.assertRaisesRegex(ValueError, \"Enum Country has no value defined for name 'ZZ'\"):\n            stream.languages.append('en-Zzzz-ZZ')\n        with self.assertRaisesRegex(AssertionError, \"Failed to parse language tag: en-Zzz-US\"):\n            stream.languages.append('en-Zzz-US')\n\n\nclass TestTags(TestCase):\n\n    def test_normalize_tags(self):\n        claim = Claim()\n\n        claim.channel.update(tags=['Anime', 'anime', ' aNiMe', 'maNGA '])\n        self.assertCountEqual(claim.channel.tags, ['anime', 'manga'])\n\n        claim.channel.update(tags=['Juri', 'juRi'])\n        self.assertCountEqual(claim.channel.tags, ['anime', 'manga', 'juri'])\n\n        claim.channel.update(tags='Anime')\n        self.assertCountEqual(claim.channel.tags, ['anime', 'manga', 'juri'])\n\n        claim.channel.update(clear_tags=True)\n        self.assertEqual(len(claim.channel.tags), 0)\n\n        claim.channel.update(tags='Anime')\n        self.assertEqual(claim.channel.tags, ['anime'])\n\n\nclass TestCollection(TestCase):\n\n    def test_collection(self):\n        collection = Collection()\n\n        collection.update(claims=['abc123', 'def123'])\n        self.assertListEqual(collection.claims.ids, ['abc123', 'def123'])\n\n        collection.update(claims=['abc123', 'bbb123'])\n        self.assertListEqual(collection.claims.ids, ['abc123', 'def123', 'abc123', 'bbb123'])\n\n        collection.update(clear_claims=True, claims=['bbb987', 'bb'])\n        self.assertListEqual(collection.claims.ids, ['bbb987', 'bb'])\n\n        self.assertEqual(collection.to_dict(), {'claims': ['bbb987', 'bb']})\n\n        collection.update(clear_claims=True)\n        self.assertListEqual(collection.claims.ids, [])\n\n\nclass TestLocations(TestCase):\n\n    def test_location_successful_parsing(self):\n        # from simple string\n        stream = Stream()\n        stream.locations.append('US')\n        self.assertEqual(stream.locations[0].country, 'US')\n\n        # from full string\n        stream = Stream()\n        stream.locations.append('US:NH:Manchester:03101:42.990605:-71.460989')\n        self.assertEqual(stream.locations[0].country, 'US')\n        self.assertEqual(stream.locations[0].state, 'NH')\n        self.assertEqual(stream.locations[0].city, 'Manchester')\n        self.assertEqual(stream.locations[0].code, '03101')\n        self.assertEqual(stream.locations[0].latitude, '42.990605')\n        self.assertEqual(stream.locations[0].longitude, '-71.460989')\n\n        # from partial string\n        stream = Stream()\n        stream.locations.append('::Manchester:03101:')\n        self.assertIsNone(stream.locations[0].country)\n        self.assertEqual(stream.locations[0].state, '')\n        self.assertEqual(stream.locations[0].city, 'Manchester')\n        self.assertEqual(stream.locations[0].code, '03101')\n        self.assertIsNone(stream.locations[0].latitude)\n        self.assertIsNone(stream.locations[0].longitude)\n\n        # from partial string lat/long\n        stream = Stream()\n        stream.locations.append('::::42.990605:-71.460989')\n        self.assertIsNone(stream.locations[0].country)\n        self.assertEqual(stream.locations[0].state, '')\n        self.assertEqual(stream.locations[0].city, '')\n        self.assertEqual(stream.locations[0].code, '')\n        self.assertEqual(stream.locations[0].latitude, '42.990605')\n        self.assertEqual(stream.locations[0].longitude, '-71.460989')\n\n        # from short circuit lat/long\n        stream = Stream()\n        stream.locations.append('42.990605:-71.460989')\n        self.assertIsNone(stream.locations[0].country)\n        self.assertEqual(stream.locations[0].state, '')\n        self.assertEqual(stream.locations[0].city, '')\n        self.assertEqual(stream.locations[0].code, '')\n        self.assertEqual(stream.locations[0].latitude, '42.990605')\n        self.assertEqual(stream.locations[0].longitude, '-71.460989')\n\n        # from json string\n        stream = Stream()\n        stream.locations.append('{\"country\": \"ES\"}')\n        self.assertEqual(stream.locations[0].country, 'ES')\n\n        # from dict\n        stream = Stream()\n        stream.locations.append({\"country\": \"UA\"})\n        self.assertEqual(stream.locations[0].country, 'UA')\n\n\nclass TestStreamUpdating(TestCase):\n\n    def test_stream_update(self):\n        stream = Stream()\n        # each of these values is set differently inside of .update()\n        stream.update(\n            title=\"foo\",\n            thumbnail_url=\"somescheme:some/path\",\n            file_name=\"file-name\"\n        )\n        self.assertEqual(stream.title, \"foo\")\n        self.assertEqual(stream.thumbnail.url, \"somescheme:some/path\")\n        self.assertEqual(stream.source.name, \"file-name\")\n        with self.assertRaises(InputValueIsNoneError):\n            stream.update(title=None)\n        with self.assertRaises(InputValueIsNoneError):\n            stream.update(file_name=None)\n        with self.assertRaises(InputValueIsNoneError):\n            stream.update(thumbnail_url=None)\n"
  },
  {
    "path": "tests/unit/schema/test_tags.py",
    "content": "import unittest\n\nfrom lbry.schema.tags import normalize_tag, clean_tags\n\n\nclass TestTagNormalization(unittest.TestCase):\n\n    def assertNormalizedTag(self, clean, dirty):\n        self.assertEqual(clean, normalize_tag(dirty))\n\n    def test_normalize_tag(self):\n        tag = self.assertNormalizedTag\n        tag('', ' \\t #!~')\n        tag('tag', 'T\\'ag')\n        tag('t ag', '\\tT  \\nAG   ')\n        tag('tag hash', '#tag~#hash!')\n\n    def test_clean_tags(self):\n        self.assertEqual(['tag'], clean_tags([' \\t #!~', '!taG', '\\t']))\n        cleaned = clean_tags(['fOo', '!taG', 'FoO'])\n        self.assertIn('tag', cleaned)\n        self.assertIn('foo', cleaned)\n        self.assertEqual(len(cleaned), 2)\n"
  },
  {
    "path": "tests/unit/schema/test_url.py",
    "content": "import unittest\n\nfrom lbry.schema.url import URL\n\n\nclaim_id = \"63f2da17b0d90042c559cc73b6b17f853945c43e\"\n\n\nclass TestURLParsing(unittest.TestCase):\n\n    segments = 'stream', 'channel'\n    fields = 'name', 'claim_id', 'amount_order'\n\n    def _assert_url(self, url_string, strictly=True, **kwargs):\n        url = URL.parse(url_string)\n        if strictly:\n            if url_string.startswith('lbry://'):\n                self.assertEqual(url_string, str(url))\n            else:\n                self.assertEqual(f'lbry://{url_string}', str(url))\n        present = {}\n        for key in kwargs:\n            for segment_name in self.segments:\n                if key.startswith(segment_name):\n                    present[segment_name] = True\n                    break\n        for segment_name in self.segments:\n            segment = getattr(url, segment_name)\n            if segment_name not in present:\n                self.assertIsNone(segment)\n            else:\n                for field in self.fields:\n                    self.assertEqual(\n                        getattr(segment, field),\n                        kwargs.get(f'{segment_name}_{field}', None)\n                    )\n\n    def _fail_url(self, url):\n        with self.assertRaisesRegex(ValueError, 'Invalid LBRY URL'):\n            URL.parse(url)\n\n    def test_parser_valid_urls(self):\n        url = self._assert_url\n        # stream\n        url('test', stream_name='test')\n        url('test*1', stream_name='test*1')\n        url('test$1', stream_name='test', stream_amount_order='1')\n        url(f'test#{claim_id}', stream_name='test', stream_claim_id=claim_id, strictly=False)\n        url(f'test:{claim_id}', stream_name='test', stream_claim_id=claim_id)\n        # channel\n        url('@test', channel_name='@test')\n        url('@test$1', channel_name='@test', channel_amount_order='1')\n        url(f'@test#{claim_id}', channel_name='@test', channel_claim_id=claim_id, strictly=False)\n        url(f'@test:{claim_id}', channel_name='@test', channel_claim_id=claim_id)\n        # channel/stream\n        url('lbry://@test/stuff', channel_name='@test', stream_name='stuff')\n        url('lbry://@test$1/stuff', channel_name='@test', channel_amount_order='1', stream_name='stuff')\n        url(f'lbry://@test#{claim_id}/stuff', channel_name='@test', channel_claim_id=claim_id, stream_name='stuff', strictly=False)\n        url(f'lbry://@test:{claim_id}/stuff', channel_name='@test', channel_claim_id=claim_id, stream_name='stuff')\n        # combined legacy and new\n        url('@test:1/stuff#2', channel_claim_id='1', stream_claim_id='2', channel_name='@test', stream_name='stuff', strictly=False)\n        # unicode regex edges\n        _url = lambda name: url(name, stream_name=name)\n        _url('\\uD799')\n        _url('\\uE000')\n        _url('\\uFFFD')\n\n    def test_parser_invalid_urls(self):\n        fail = self._fail_url\n        fail(\"lbry://\")\n        fail(\"lbry://\\u0000\")\n        fail(\"lbry://\\u0008\")\n        fail(\"lbry://\\u000b\")\n        fail(\"lbry://\\u000c\")\n        fail(\"lbry://\\u000e\")\n        fail(\"lbry://\\u001f\")\n        fail(\"lbry://\\uD800\")\n        fail(\"lbry://\\uDFFF\")\n        fail(\"lbry://\\uDFFE\")\n        fail(\"lbry://\\uFFFF\")\n        fail(\"lbry://;\")\n        fail(\"lbry://no\\ttab\")\n        fail(\"lbry://no space\")\n        fail(\"lbry://no\\rcr\")\n        fail(\"lbry://no\\new\\nline\")\n        fail(\"lbry://\\\"\")\n        fail(\"lbry://\\\\\")\n        fail(\"lbry:///\")\n        fail(\"lbry://<\") and fail(\"lbry://>\")\n        fail(\"lbry://{\") and fail(\"lbry://}\")\n        fail(\"lbry://[\") and fail(\"lbry://]\")\n        fail(\"lbry://%\")\n        fail(\"lbry://|\")\n        fail(\"lbry://^\")\n        fail(\"lbry://~\")\n        fail(\"lbry://`\")\n        fail(\"lbry://test:3$1\")\n        fail(\"lbry://test$1:1\")\n        fail(\"lbry://test#x\")\n        fail(\"lbry://test#x/page\")\n        fail(\"lbry://test$\")\n        fail(\"lbry://test#\")\n        fail(\"lbry://test:\")\n        fail(\"lbry://test$x\")\n        fail(\"lbry://test:x\")\n        fail(\"lbry://@test@\")\n        fail(\"lbry://@test:\")\n        fail(\"lbry://test@\")\n        fail(\"lbry://tes@t\")\n        fail(f\"lbry://test:1#{claim_id}\")\n        fail(\"lbry://test$0\")\n        fail(\"lbry://test/path\")\n        fail(\"lbry://test:1:1:1\")\n        fail(\"whatever/lbry://test\")\n        fail(\"lbry://lbry://test\")\n        fail(\"lbry://@/what\")\n        fail(\"lbry://abc:0x123\")\n        fail(\"lbry://abc:0x123/page\")\n        fail(\"lbry://@test1#ABCDEF/fakepath\")\n        fail(\"lbry://@test1$1/fakepath?arg1&arg2&arg3\")\n"
  },
  {
    "path": "tests/unit/stream/__init__.py",
    "content": ""
  },
  {
    "path": "tests/unit/stream/test_managed_stream.py",
    "content": "import os\nimport shutil\nimport unittest\nfrom unittest import mock\nimport asyncio\nfrom lbry.blob.blob_file import MAX_BLOB_SIZE\nfrom lbry.blob_exchange.serialization import BlobResponse\nfrom lbry.blob_exchange.server import BlobServerProtocol\nfrom lbry.dht.node import Node\nfrom lbry.dht.peer import make_kademlia_peer\nfrom lbry.extras.daemon.storage import StoredContentClaim\nfrom lbry.schema import Claim\nfrom lbry.stream.managed_stream import ManagedStream\nfrom lbry.stream.descriptor import StreamDescriptor\nfrom tests.unit.blob_exchange.test_transfer_blob import BlobExchangeTestBase\n\n\nclass TestManagedStream(BlobExchangeTestBase):\n    async def create_stream(self, blob_count: int = 10, file_name='test_file'):\n        self.stream_bytes = b''\n        for _ in range(blob_count):\n            self.stream_bytes += os.urandom(MAX_BLOB_SIZE - 1)\n        # create the stream\n        file_path = os.path.join(self.server_dir, file_name)\n        with open(file_path, 'wb') as f:\n            f.write(self.stream_bytes)\n        descriptor = await StreamDescriptor.create_stream(self.loop, self.server_blob_manager.blob_dir, file_path)\n        descriptor.suggested_file_name = file_name\n        descriptor.stream_hash = descriptor.get_stream_hash()\n        self.sd_hash = descriptor.sd_hash = descriptor.calculate_sd_hash()\n        await descriptor.make_sd_blob()\n        return descriptor\n\n    async def setup_stream(self, blob_count: int = 10):\n        await self.create_stream(blob_count)\n        self.stream = ManagedStream(\n            self.loop, self.client_config, self.client_blob_manager, self.sd_hash, self.client_dir\n        )\n\n    async def test_client_sanitizes_file_name(self):\n        illegal_name = 't<?t_f:|<'\n        descriptor = await self.create_stream(file_name=illegal_name)\n        descriptor.suggested_file_name = illegal_name\n        self.stream = ManagedStream(\n            self.loop, self.client_config, self.client_blob_manager, self.sd_hash, self.client_dir\n        )\n        await self._test_transfer_stream(10, skip_setup=True)\n        self.assertTrue(self.stream.completed)\n        self.assertEqual(self.stream.file_name, 'tt_f')\n        self.assertTrue(self.stream.output_file_exists)\n        self.assertTrue(os.path.isfile(self.stream.full_path))\n        self.assertEqual(self.stream.full_path, os.path.join(self.client_dir, 'tt_f'))\n        self.assertTrue(os.path.isfile(os.path.join(self.client_dir, 'tt_f')))\n\n    async def test_empty_name_fallback(self):\n        descriptor = await self.create_stream(file_name=\" \")\n        descriptor.suggested_file_name = \" \"\n        claim = Claim()\n        claim.stream.source.name = \"cool.mp4\"\n        self.stream = ManagedStream(\n            self.loop, self.client_config, self.client_blob_manager, self.sd_hash, self.client_dir,\n            claim=StoredContentClaim(serialized=claim.to_bytes().hex())\n        )\n        await self._test_transfer_stream(10, skip_setup=True)\n        self.assertTrue(self.stream.completed)\n        self.assertEqual(self.stream.suggested_file_name, \"cool.mp4\")\n        self.assertEqual(self.stream.stream_name, \"cool.mp4\")\n        self.assertEqual(self.stream.mime_type, \"video/mp4\")\n\n    async def test_status_file_completed(self):\n        await self._test_transfer_stream(10)\n        self.assertTrue(self.stream.output_file_exists)\n        self.assertTrue(self.stream.completed)\n        with open(self.stream.full_path, 'w+b') as outfile:\n            outfile.truncate(1)\n        self.assertTrue(self.stream.output_file_exists)\n        self.assertFalse(self.stream.completed)\n\n    async def _test_transfer_stream(self, blob_count: int, mock_accumulate_peers=None, stop_when_done=True,\n                                    skip_setup=False):\n        if not skip_setup:\n            await self.setup_stream(blob_count)\n        mock_node = mock.Mock(spec=Node)\n\n        def _mock_accumulate_peers(q1, q2):\n            async def _task():\n                pass\n            q2.put_nowait([self.server_from_client])\n            return q2, self.loop.create_task(_task())\n\n        mock_node.accumulate_peers = mock_accumulate_peers or _mock_accumulate_peers\n        self.stream.downloader.node = mock_node\n        await self.stream.save_file()\n        await self.stream.finished_write_attempt.wait()\n        self.assertTrue(os.path.isfile(self.stream.full_path))\n        if stop_when_done:\n            await self.stream.stop()\n        self.assertTrue(os.path.isfile(self.stream.full_path))\n        with open(self.stream.full_path, 'rb') as f:\n            self.assertEqual(f.read(), self.stream_bytes)\n        await asyncio.sleep(0.01)\n\n    async def test_transfer_stream(self):\n        await self._test_transfer_stream(10)\n        self.assertEqual(self.stream.status, \"finished\")\n        self.assertFalse(self.stream._running.is_set())\n\n    async def test_delayed_stop(self):\n        await self._test_transfer_stream(10, stop_when_done=False)\n        self.assertEqual(self.stream.status, \"finished\")\n        self.assertTrue(self.stream._running.is_set())\n        await asyncio.sleep(0.5)\n        self.assertTrue(self.stream._running.is_set())\n        await asyncio.sleep(2)\n        self.assertEqual(self.stream.status, \"finished\")\n        self.assertFalse(self.stream._running.is_set())\n\n    @unittest.SkipTest\n    async def test_transfer_hundred_blob_stream(self):\n        await self._test_transfer_stream(100)\n\n    async def test_transfer_stream_bad_first_peer_good_second(self):\n        await self.setup_stream(2)\n\n        mock_node = mock.Mock(spec=Node)\n\n        bad_peer = make_kademlia_peer(b'2' * 48, \"127.0.0.1\", tcp_port=3334, allow_localhost=True)\n\n        def _mock_accumulate_peers(q1, q2):\n            async def _task():\n                pass\n\n            q2.put_nowait([bad_peer])\n            self.loop.call_later(1, q2.put_nowait, [self.server_from_client])\n            return q2, self.loop.create_task(_task())\n\n        mock_node.accumulate_peers = _mock_accumulate_peers\n\n        self.stream.downloader.node = mock_node\n        await self.stream.save_file()\n        await self.stream.finished_writing.wait()\n        self.assertTrue(os.path.isfile(self.stream.full_path))\n        with open(self.stream.full_path, 'rb') as f:\n            self.assertEqual(f.read(), self.stream_bytes)\n        await self.stream.stop()\n        # self.assertIs(self.server_from_client.tcp_last_down, None)\n        # self.assertIsNot(bad_peer.tcp_last_down, None)\n\n    async def test_client_chunked_response(self):\n        self.server.stop_server()\n\n        class ChunkedServerProtocol(BlobServerProtocol):\n            def send_response(self, responses):\n                to_send = []\n                while responses:\n                    to_send.append(responses.pop())\n                for byte in BlobResponse(to_send).serialize():\n                    self.transport.write(bytes([byte]))\n        self.server.server_protocol_class = ChunkedServerProtocol\n        self.server.start_server(33333, '127.0.0.1')\n        self.assertEqual(0, len(self.client_blob_manager.completed_blob_hashes))\n        await asyncio.wait_for(self._test_transfer_stream(10), timeout=2)\n        self.assertEqual(11, len(self.client_blob_manager.completed_blob_hashes))\n\n    async def test_create_and_decrypt_one_blob_stream(self, blobs=1, corrupt=False):\n        descriptor = await self.create_stream(blobs)\n\n        # copy blob files\n        shutil.copy(os.path.join(self.server_blob_manager.blob_dir, self.sd_hash),\n                    os.path.join(self.client_blob_manager.blob_dir, self.sd_hash))\n        self.stream = ManagedStream(self.loop, self.client_config, self.client_blob_manager, self.sd_hash,\n                                    self.client_dir)\n\n        for blob_info in descriptor.blobs[:-1]:\n            shutil.copy(os.path.join(self.server_blob_manager.blob_dir, blob_info.blob_hash),\n                        os.path.join(self.client_blob_manager.blob_dir, blob_info.blob_hash))\n            if corrupt and blob_info.length == MAX_BLOB_SIZE:\n                with open(os.path.join(self.client_blob_manager.blob_dir, blob_info.blob_hash), \"rb+\") as handle:\n                    handle.truncate()\n                    handle.flush()\n        await self.stream.save_file()\n        await self.stream.finished_writing.wait()\n        if corrupt:\n            return self.assertFalse(os.path.isfile(os.path.join(self.client_dir, \"test_file\")))\n\n        with open(os.path.join(self.client_dir, \"test_file\"), \"rb\") as f:\n            decrypted = f.read()\n        self.assertEqual(decrypted, self.stream_bytes)\n\n        self.assertTrue(self.client_blob_manager.get_blob(self.sd_hash).get_is_verified())\n        self.assertEqual(\n            True, self.client_blob_manager.get_blob(self.stream.descriptor.blobs[0].blob_hash).get_is_verified()\n        )\n        #\n        # # its all blobs + sd blob - last blob, which is the same size as descriptor.blobs\n        # self.assertEqual(len(descriptor.blobs), len(await downloader_storage.get_all_finished_blobs()))\n        # self.assertEqual(\n        #     [descriptor.sd_hash, descriptor.blobs[0].blob_hash], await downloader_storage.get_blobs_to_announce()\n        # )\n        #\n        # await downloader_storage.close()\n        # await self.storage.close()\n\n    async def test_create_and_decrypt_multi_blob_stream(self):\n        await self.test_create_and_decrypt_one_blob_stream(10)\n\n    # async def test_create_truncate_and_handle_stream(self):\n    #     # The purpose of this test is just to make sure it can finish even if a blob is corrupt/truncated\n    #     await asyncio.wait_for(self.test_create_and_decrypt_one_blob_stream(corrupt=True), timeout=5)\n"
  },
  {
    "path": "tests/unit/stream/test_reflector.py",
    "content": "import os\nimport asyncio\nimport tempfile\nimport shutil\nfrom lbry.testcase import AsyncioTestCase\nfrom lbry.conf import Config\nfrom lbry.extras.daemon.storage import SQLiteStorage\nfrom lbry.blob.blob_manager import BlobManager\nfrom lbry.stream.stream_manager import StreamManager\nfrom lbry.stream.reflector.server import ReflectorServer\n\n\nclass TestReflector(AsyncioTestCase):\n    async def asyncSetUp(self):\n        self.loop = asyncio.get_event_loop()\n        self.key = b'deadbeef' * 4\n        self.cleartext = os.urandom(20000000)\n\n        tmp_dir = tempfile.mkdtemp()\n        self.addCleanup(lambda: shutil.rmtree(tmp_dir))\n        self.conf = Config()\n        self.storage = SQLiteStorage(self.conf, os.path.join(tmp_dir, \"lbrynet.sqlite\"))\n        await self.storage.open()\n        self.blob_manager = BlobManager(self.loop, tmp_dir, self.storage, self.conf)\n        self.addCleanup(self.blob_manager.stop)\n        self.stream_manager = StreamManager(self.loop, Config(), self.blob_manager, None, self.storage, None)\n\n        server_tmp_dir = tempfile.mkdtemp()\n        self.addCleanup(lambda: shutil.rmtree(server_tmp_dir))\n        self.server_conf = Config()\n        self.server_storage = SQLiteStorage(self.server_conf, os.path.join(server_tmp_dir, \"lbrynet.sqlite\"))\n        await self.server_storage.open()\n        self.server_blob_manager = BlobManager(self.loop, server_tmp_dir, self.server_storage, self.server_conf)\n        self.addCleanup(self.server_blob_manager.stop)\n\n        download_dir = tempfile.mkdtemp()\n        self.addCleanup(lambda: shutil.rmtree(download_dir))\n\n        # create the stream\n        file_path = os.path.join(tmp_dir, \"test_file\")\n        with open(file_path, 'wb') as f:\n            f.write(self.cleartext)\n        self.stream_manager.config.reflect_streams = False\n        self.stream = await self.stream_manager.create(file_path)\n\n    async def _test_reflect_stream(self, response_chunk_size=50, partial_needs=False):\n        reflector = ReflectorServer(self.server_blob_manager, response_chunk_size=response_chunk_size,\n                                    partial_needs=partial_needs)\n        reflector.start_server(5566, '127.0.0.1')\n        if partial_needs:\n            server_blob = self.server_blob_manager.get_blob(self.stream.sd_hash)\n            client_blob = self.blob_manager.get_blob(self.stream.sd_hash)\n            with client_blob.reader_context() as handle:\n                server_blob.set_length(client_blob.get_length())\n                writer = server_blob.get_blob_writer('nobody', 0)\n                writer.write(handle.read())\n            self.server_blob_manager.blob_completed(server_blob)\n        await reflector.started_listening.wait()\n        self.addCleanup(reflector.stop_server)\n        self.assertEqual(0, self.stream.reflector_progress)\n        sent = await self.stream.upload_to_reflector('127.0.0.1', 5566)\n        self.assertEqual(100, self.stream.reflector_progress)\n        if partial_needs:\n            self.assertFalse(self.stream.is_fully_reflected)\n            send_more = await self.stream.upload_to_reflector('127.0.0.1', 5566)\n            self.assertGreater(len(send_more), 0)\n            sent.extend(send_more)\n            sent.append(self.stream.sd_hash)\n        self.assertSetEqual(\n            set(sent),\n            set(map(lambda b: b.blob_hash,\n                    self.stream.descriptor.blobs[:-1] + [self.blob_manager.get_blob(self.stream.sd_hash)]))\n        )\n        send_more = await self.stream.upload_to_reflector('127.0.0.1', 5566)\n        self.assertEqual(len(send_more), 0)\n        self.assertTrue(self.stream.is_fully_reflected)\n        server_sd_blob = self.server_blob_manager.get_blob(self.stream.sd_hash)\n        self.assertTrue(server_sd_blob.get_is_verified())\n        self.assertEqual(server_sd_blob.length, server_sd_blob.length)\n        for blob in self.stream.descriptor.blobs[:-1]:\n            server_blob = self.server_blob_manager.get_blob(blob.blob_hash)\n            self.assertTrue(server_blob.get_is_verified())\n            self.assertEqual(server_blob.length, blob.length)\n\n        sent = await self.stream.upload_to_reflector('127.0.0.1', 5566)\n        self.assertListEqual(sent, [])\n\n    async def test_reflect_stream(self):\n        return await asyncio.wait_for(self._test_reflect_stream(response_chunk_size=50), 3)\n\n    async def test_reflect_stream_but_reflector_changes_its_mind(self):\n        return await asyncio.wait_for(self._test_reflect_stream(partial_needs=True), 3)\n\n    async def test_reflect_stream_small_response_chunks(self):\n        return await asyncio.wait_for(self._test_reflect_stream(response_chunk_size=30), 3)\n\n    async def test_announces(self):\n        to_announce = await self.storage.get_blobs_to_announce()\n        self.assertIn(self.stream.sd_hash, to_announce, \"sd blob not set to announce\")\n        self.assertNotIn(self.stream.descriptor.blobs[0].blob_hash, to_announce, \"head blob set to announce\")\n\n    async def test_result_from_disconnect_mid_sd_transfer(self):\n        stop = asyncio.Event()\n        incoming = asyncio.Event()\n        reflector = ReflectorServer(\n            self.server_blob_manager, response_chunk_size=50, stop_event=stop, incoming_event=incoming\n        )\n        reflector.start_server(5566, '127.0.0.1')\n        await reflector.started_listening.wait()\n        self.addCleanup(reflector.stop_server)\n        self.assertEqual(0, self.stream.reflector_progress)\n        reflect_task = asyncio.create_task(self.stream.upload_to_reflector('127.0.0.1', 5566))\n        await incoming.wait()\n        stop.set()\n        # this used to raise (and then propagate) a CancelledError\n        self.assertListEqual(await reflect_task, [])\n        self.assertFalse(self.stream.is_fully_reflected)\n        self.assertFalse(self.server_blob_manager.get_blob(self.stream.sd_hash).get_is_verified())\n\n    async def test_result_from_disconnect_after_sd_transfer(self):\n        stop = asyncio.Event()\n        incoming = asyncio.Event()\n        not_incoming = asyncio.Event()\n        reflector = ReflectorServer(\n            self.server_blob_manager, response_chunk_size=50, stop_event=stop, incoming_event=incoming,\n            not_incoming_event=not_incoming\n        )\n        reflector.start_server(5566, '127.0.0.1')\n        await reflector.started_listening.wait()\n        self.addCleanup(reflector.stop_server)\n        self.assertEqual(0, self.stream.reflector_progress)\n        reflect_task = asyncio.create_task(self.stream.upload_to_reflector('127.0.0.1', 5566))\n        await incoming.wait()\n        await not_incoming.wait()\n        stop.set()\n        sent = await reflect_task\n        self.assertListEqual([self.stream.sd_hash], sent)\n        self.assertTrue(self.server_blob_manager.get_blob(self.stream.sd_hash).get_is_verified())\n        self.assertFalse(self.stream.is_fully_reflected)\n\n    async def test_result_from_disconnect_after_data_transfer(self):\n        stop = asyncio.Event()\n        incoming = asyncio.Event()\n        not_incoming = asyncio.Event()\n        reflector = ReflectorServer(\n            self.server_blob_manager, response_chunk_size=50, stop_event=stop, incoming_event=incoming,\n            not_incoming_event=not_incoming\n        )\n        reflector.start_server(5566, '127.0.0.1')\n        await reflector.started_listening.wait()\n        self.addCleanup(reflector.stop_server)\n        self.assertEqual(0, self.stream.reflector_progress)\n        reflect_task = asyncio.create_task(self.stream.upload_to_reflector('127.0.0.1', 5566))\n        await incoming.wait()\n        await not_incoming.wait()\n        await incoming.wait()\n        await not_incoming.wait()\n        stop.set()\n        sent = await reflect_task\n        self.assertListEqual([self.stream.sd_hash, self.stream.descriptor.blobs[0].blob_hash], sent)\n        self.assertTrue(self.server_blob_manager.get_blob(self.stream.sd_hash).get_is_verified())\n        self.assertTrue(self.server_blob_manager.get_blob(self.stream.descriptor.blobs[0].blob_hash).get_is_verified())\n        self.assertFalse(self.stream.is_fully_reflected)\n\n    async def test_result_from_disconnect_mid_data_transfer(self):\n        stop = asyncio.Event()\n        incoming = asyncio.Event()\n        not_incoming = asyncio.Event()\n        reflector = ReflectorServer(\n            self.server_blob_manager, response_chunk_size=50, stop_event=stop, incoming_event=incoming,\n            not_incoming_event=not_incoming\n        )\n        reflector.start_server(5566, '127.0.0.1')\n        await reflector.started_listening.wait()\n        self.addCleanup(reflector.stop_server)\n        self.assertEqual(0, self.stream.reflector_progress)\n        reflect_task = asyncio.create_task(self.stream.upload_to_reflector('127.0.0.1', 5566))\n        await incoming.wait()\n        await not_incoming.wait()\n        await incoming.wait()\n        stop.set()\n        self.assertListEqual(await reflect_task, [self.stream.sd_hash])\n        self.assertTrue(self.server_blob_manager.get_blob(self.stream.sd_hash).get_is_verified())\n        self.assertFalse(\n            self.server_blob_manager.get_blob(self.stream.descriptor.blobs[0].blob_hash).get_is_verified()\n        )\n        self.assertFalse(self.stream.is_fully_reflected)\n\n    async def test_delete_file_during_reflector_upload(self):\n        stop = asyncio.Event()\n        incoming = asyncio.Event()\n        not_incoming = asyncio.Event()\n        reflector = ReflectorServer(\n            self.server_blob_manager, response_chunk_size=50, stop_event=stop, incoming_event=incoming,\n            not_incoming_event=not_incoming\n        )\n        reflector.start_server(5566, '127.0.0.1')\n        await reflector.started_listening.wait()\n        self.addCleanup(reflector.stop_server)\n        self.assertEqual(0, self.stream.reflector_progress)\n        reflect_task = asyncio.create_task(self.stream.upload_to_reflector('127.0.0.1', 5566))\n        await incoming.wait()\n        await not_incoming.wait()\n        await incoming.wait()\n        await self.stream_manager.delete(self.stream, delete_file=True)\n        # this used to raise OSError when it can't read the deleted blob for the upload\n        sent = await reflect_task\n        self.assertListEqual([self.stream.sd_hash], sent)\n        self.assertTrue(self.server_blob_manager.get_blob(self.stream.sd_hash).get_is_verified())\n        self.assertFalse(\n            self.server_blob_manager.get_blob(self.stream.descriptor.blobs[0].blob_hash).get_is_verified()\n        )\n        self.assertFalse(self.stream.is_fully_reflected)\n"
  },
  {
    "path": "tests/unit/stream/test_stream_descriptor.py",
    "content": "import os\nimport asyncio\nimport tempfile\nimport shutil\nimport json\n\nfrom lbry.blob.blob_file import BlobFile\nfrom lbry.testcase import AsyncioTestCase\nfrom lbry.conf import Config\nfrom lbry.error import InvalidStreamDescriptorError\nfrom lbry.extras.daemon.storage import SQLiteStorage\nfrom lbry.blob.blob_manager import BlobManager\nfrom lbry.stream.descriptor import StreamDescriptor, sanitize_file_name\n\n\nclass TestStreamDescriptor(AsyncioTestCase):\n    async def asyncSetUp(self):\n        self.loop = asyncio.get_event_loop()\n        self.key = b'deadbeef' * 4\n        self.cleartext = os.urandom(20000000)\n        self.tmp_dir = tempfile.mkdtemp()\n        self.addCleanup(lambda: shutil.rmtree(self.tmp_dir))\n        self.conf = Config()\n        self.storage = SQLiteStorage(self.conf, \":memory:\")\n        await self.storage.open()\n        self.blob_manager = BlobManager(self.loop, self.tmp_dir, self.storage, self.conf)\n\n        self.file_path = os.path.join(self.tmp_dir, \"test_file\")\n        with open(self.file_path, 'wb') as f:\n            f.write(self.cleartext)\n\n        self.descriptor = await StreamDescriptor.create_stream(self.loop, self.tmp_dir, self.file_path, key=self.key)\n        self.sd_hash = self.descriptor.calculate_sd_hash()\n        self.sd_dict = json.loads(self.descriptor.as_json())\n\n    def _write_sd(self):\n        with open(os.path.join(self.tmp_dir, self.sd_hash), 'wb') as f:\n            f.write(json.dumps(self.sd_dict, sort_keys=True).encode())\n\n    async def _test_invalid_sd(self):\n        self._write_sd()\n        with self.assertRaises(InvalidStreamDescriptorError):\n            await self.blob_manager.get_stream_descriptor(self.sd_hash)\n\n    async def test_load_sd_blob(self):\n        self._write_sd()\n        descriptor = await self.blob_manager.get_stream_descriptor(self.sd_hash)\n        self.assertEqual(descriptor.calculate_sd_hash(), self.sd_hash)\n\n    async def test_missing_terminator(self):\n        self.sd_dict['blobs'].pop()\n        await self._test_invalid_sd()\n\n    async def test_terminator_not_at_end(self):\n        terminator = self.sd_dict['blobs'].pop()\n        self.sd_dict['blobs'] = [terminator] + self.sd_dict['blobs']\n        await self._test_invalid_sd()\n\n    async def test_terminator_has_blob_hash(self):\n        self.sd_dict['blobs'][-1]['blob_hash'] = '1' * 96\n        await self._test_invalid_sd()\n\n    async def test_blob_order(self):\n        terminator = self.sd_dict['blobs'].pop()\n        self.sd_dict['blobs'].reverse()\n        self.sd_dict['blobs'].append(terminator)\n        await self._test_invalid_sd()\n\n    async def test_skip_blobs(self):\n        self.sd_dict['blobs'][-2]['blob_num'] = self.sd_dict['blobs'][-2]['blob_num'] + 1\n        await self._test_invalid_sd()\n\n    async def test_invalid_stream_hash(self):\n        self.sd_dict['blobs'][-2]['blob_hash'] = '1' * 96\n        await self._test_invalid_sd()\n\n    async def test_zero_length_blob(self):\n        self.sd_dict['blobs'][-2]['length'] = 0\n        await self._test_invalid_sd()\n\n    def test_sanitize_file_name(self):\n        self.assertEqual(sanitize_file_name(' t/-?t|.g.ext '), 't-t.g.ext')\n        self.assertEqual(sanitize_file_name('end_dot .'), 'end_dot')\n        self.assertEqual(sanitize_file_name('.file\\0\\0'), '.file')\n        self.assertEqual(sanitize_file_name('test n\\16ame.ext'), 'test name.ext')\n        self.assertEqual(sanitize_file_name('COM8.ext', default_file_name='default1'), 'default1.ext')\n        self.assertEqual(sanitize_file_name('LPT2', default_file_name='default2'), 'default2')\n        self.assertEqual(sanitize_file_name('', default_file_name=''), '')\n\n\nclass TestRecoverOldStreamDescriptors(AsyncioTestCase):\n    async def test_old_key_sort_sd_blob(self):\n        loop = asyncio.get_event_loop()\n        tmp_dir = tempfile.mkdtemp()\n        self.addCleanup(lambda: shutil.rmtree(tmp_dir))\n        self.conf = Config()\n        storage = SQLiteStorage(self.conf, \":memory:\")\n        await storage.open()\n        blob_manager = BlobManager(loop, tmp_dir, storage, self.conf)\n\n        sd_bytes = b'{\"stream_name\": \"4f62616d6120446f6e6b65792d322e73746c\", \"blobs\": [{\"length\": 1153488, \"blob_num' \\\n                   b'\": 0, \"blob_hash\": \"9fa32a249ce3f2d4e46b78599800f368b72f2a7f22b81df443c7f6bdbef496bd61b4c0079c7' \\\n                   b'3d79c8bb9be9a6bf86592\", \"iv\": \"0bf348867244019c9e22196339016ea6\"}, {\"length\": 0, \"blob_num\": 1,' \\\n                   b' \"iv\": \"9f36abae16955463919b07ed530a3d18\"}], \"stream_type\": \"lbryfile\", \"key\": \"a03742b87628aa7' \\\n                   b'228e48f1dcd207e48\", \"suggested_file_name\": \"4f62616d6120446f6e6b65792d322e73746c\", \"stream_hash' \\\n                   b'\": \"b43f4b1379780caf60d20aa06ac38fb144df61e514ebfa97537018ba73bce8fe37ae712f473ff0ba0be0eef44e1' \\\n                   b'60207\"}'\n        sd_hash = '9313d1807551186126acc3662e74d9de29cede78d4f133349ace846273ef116b9bb86be86c54509eb84840e4b032f6b2'\n        stream_hash = 'b43f4b1379780caf60d20aa06ac38fb144df61e514ebfa97537018ba73bce8fe37ae712f473ff0ba0be0eef44e160207'\n\n        blob = blob_manager.get_blob(sd_hash)\n        blob.set_length(len(sd_bytes))\n        writer = blob.get_blob_writer()\n        writer.write(sd_bytes)\n        await blob.verified.wait()\n        descriptor = await StreamDescriptor.from_stream_descriptor_blob(\n            loop, blob_manager.blob_dir, blob\n        )\n        self.assertEqual(stream_hash, descriptor.get_stream_hash())\n        self.assertEqual(sd_hash, descriptor.calculate_old_sort_sd_hash())\n        self.assertNotEqual(sd_hash, descriptor.calculate_sd_hash())\n\n    async def test_decode_corrupt_blob_raises_proper_exception_and_deletes_corrupt_file(self):\n        loop = asyncio.get_event_loop()\n        tmp_dir = tempfile.mkdtemp()\n        self.addCleanup(lambda: shutil.rmtree(tmp_dir))\n        sd_hash = '9313d1807551186126acc3662e74d9de29cede78d4f133349ace846273ef116b9bb86be86c54509eb84840e4b032f6b2'\n        with open(os.path.join(tmp_dir, sd_hash), 'wb') as handle:\n            handle.write(b'doesnt work')\n        blob = BlobFile(loop, sd_hash, blob_directory=tmp_dir)\n        self.assertTrue(blob.file_exists)\n        self.assertIsNotNone(blob.length)\n        with self.assertRaises(InvalidStreamDescriptorError):\n            await StreamDescriptor.from_stream_descriptor_blob(\n                loop, tmp_dir, blob\n            )\n        self.assertFalse(blob.file_exists)\n        # fixme: this is an emergency PR, please move this to blob_file tests later\n        self.assertIsNone(blob.length)\n"
  },
  {
    "path": "tests/unit/stream/test_stream_manager.py",
    "content": "import os\nimport shutil\nimport binascii\nfrom unittest import mock\nimport asyncio\nimport json\nfrom decimal import Decimal\n\nfrom lbry.file.file_manager import FileManager\nfrom tests.unit.blob_exchange.test_transfer_blob import BlobExchangeTestBase\nfrom lbry.testcase import get_fake_exchange_rate_manager\nfrom lbry.utils import generate_id\nfrom lbry.error import InsufficientFundsError\nfrom lbry.error import KeyFeeAboveMaxAllowedError, ResolveError, DownloadSDTimeoutError, DownloadDataTimeoutError\nfrom lbry.wallet import WalletManager, Wallet, Ledger, Transaction, Input, Output, Database\nfrom lbry.wallet.constants import CENT, NULL_HASH32\nfrom lbry.wallet.network import ClientSession\nfrom lbry.conf import Config\nfrom lbry.extras.daemon.analytics import AnalyticsManager\nfrom lbry.stream.stream_manager import StreamManager\nfrom lbry.stream.descriptor import StreamDescriptor\nfrom lbry.dht.node import Node\nfrom lbry.dht.protocol.protocol import KademliaProtocol\nfrom lbry.dht.protocol.routing_table import TreeRoutingTable\nfrom lbry.schema.claim import Claim\n\n\ndef get_mock_node(peer=None):\n    def mock_accumulate_peers(q1: asyncio.Queue, q2: asyncio.Queue):\n        async def _task():\n            pass\n        if peer:\n            q2.put_nowait([peer])\n        return q2, asyncio.create_task(_task())\n\n    mock_node = mock.Mock(spec=Node)\n    mock_node.protocol = mock.Mock(spec=KademliaProtocol)\n    mock_node.protocol.routing_table = mock.Mock(spec=TreeRoutingTable)\n    mock_node.protocol.routing_table.get_peers = lambda: []\n    mock_node.accumulate_peers = mock_accumulate_peers\n    mock_node.joined = asyncio.Event()\n    mock_node.joined.set()\n    return mock_node\n\n\ndef get_output(amount=CENT, pubkey_hash=NULL_HASH32):\n    return Transaction() \\\n        .add_outputs([Output.pay_pubkey_hash(amount, pubkey_hash)]) \\\n        .outputs[0]\n\n\ndef get_input():\n    return Input.spend(get_output())\n\n\ndef get_transaction(txo=None):\n    return Transaction() \\\n        .add_inputs([get_input()]) \\\n        .add_outputs([txo or Output.pay_pubkey_hash(CENT, NULL_HASH32)])\n\n\ndef get_claim_transaction(claim_name, claim=b''):\n    return get_transaction(\n        Output.pay_claim_name_pubkey_hash(CENT, claim_name, claim, NULL_HASH32)\n    )\n\n\nasync def get_mock_wallet(sd_hash, storage, wallet_dir, balance=10.0, fee=None):\n    claim = Claim()\n    if fee:\n        if fee['currency'] == 'LBC':\n            claim.stream.fee.lbc = Decimal(fee['amount'])\n        elif fee['currency'] == 'USD':\n            claim.stream.fee.usd = Decimal(fee['amount'])\n    claim.stream.title = \"33rpm\"\n    claim.stream.languages.append(\"en\")\n    claim.stream.source.sd_hash = sd_hash\n    claim.stream.source.media_type = \"image/png\"\n\n    tx = get_claim_transaction(\"33rpm\", claim.to_bytes())\n    tx.height = 514081\n    txo = tx.outputs[0]\n    txo.meta.update({\n        \"permanent_url\": \"33rpm#c49566d631226492317d06ad7fdbe1ed32925124\",\n\n    })\n\n    class FakeHeaders:\n        def estimated_timestamp(self, height):\n            return 1984\n\n        def __init__(self, height):\n            self.height = height\n\n        def __getitem__(self, item):\n            return {'timestamp': 1984}\n\n    wallet = Wallet()\n    ledger = Ledger({\n        'db': Database(os.path.join(wallet_dir, 'blockchain.db')),\n        'headers': FakeHeaders(514082),\n        'tx_cache_size': 10000\n    })\n    await ledger.db.open()\n    wallet.generate_account(ledger)\n    manager = WalletManager()\n    manager.config = Config()\n    manager.config.save_files = True\n    manager.config.transaction_cache_size = 10000\n    manager.wallets.append(wallet)\n    manager.ledgers[Ledger] = ledger\n    manager.ledger.network.client = ClientSession(\n        network=manager.ledger.network, server=('fakespv.lbry.com', 50001)\n    )\n\n    async def mock_resolve(*args, **kwargs):\n        result = {txo.meta['permanent_url']: txo}\n        await storage.save_claim_from_output(ledger, txo)\n        return result\n    manager.ledger.resolve = mock_resolve\n\n    async def get_balance(*_):\n        return balance\n    manager.get_balance = get_balance\n\n    return manager, txo.meta['permanent_url']\n\n\nclass TestStreamManager(BlobExchangeTestBase):\n    async def asyncSetUp(self):\n        await super().asyncSetUp()\n        self.client_config.share_usage_data = True\n\n    async def setup_stream_manager(self, balance=10.0, fee=None, old_sort=False):\n        file_path = os.path.join(self.server_dir, \"test_file\")\n        with open(file_path, 'wb') as f:\n            f.write(os.urandom(20000000))\n        descriptor = await StreamDescriptor.create_stream(\n            self.loop, self.server_blob_manager.blob_dir, file_path, old_sort=old_sort\n        )\n        self.sd_hash = descriptor.sd_hash\n        self.mock_wallet, self.uri = await get_mock_wallet(self.sd_hash, self.client_storage, self.client_wallet_dir,\n                                                           balance, fee)\n        analytics_manager = AnalyticsManager(\n            self.client_config,\n            binascii.hexlify(generate_id()).decode(),\n            binascii.hexlify(generate_id()).decode()\n        )\n        self.stream_manager = StreamManager(\n            self.loop, self.client_config, self.client_blob_manager, self.mock_wallet,\n            self.client_storage, get_mock_node(self.server_from_client),\n            analytics_manager\n        )\n        self.file_manager = FileManager(\n            self.loop, self.client_config, self.mock_wallet, self.client_storage, analytics_manager\n        )\n        self.file_manager.source_managers['stream'] = self.stream_manager\n        self.exchange_rate_manager = get_fake_exchange_rate_manager()\n\n    async def _test_time_to_first_bytes(self, check_post, error=None, after_setup=None):\n        await self.setup_stream_manager()\n        if after_setup:\n            after_setup()\n        checked_analytics_event = False\n\n        async def _check_post(event):\n            check_post(event)\n            nonlocal checked_analytics_event\n            checked_analytics_event = True\n\n        self.stream_manager.analytics_manager._post = _check_post\n        if error:\n            with self.assertRaises(error):\n                await self.file_manager.download_from_uri(self.uri, self.exchange_rate_manager)\n        else:\n            await self.file_manager.download_from_uri(self.uri, self.exchange_rate_manager)\n        await asyncio.sleep(0)\n        self.assertTrue(checked_analytics_event)\n\n    async def test_time_to_first_bytes(self):\n        def check_post(event):\n            self.assertEqual(event['event'], 'Time To First Bytes')\n            total_duration = event['properties']['total_duration']\n            resolve_duration = event['properties']['resolve_duration']\n            head_blob_duration = event['properties']['head_blob_duration']\n            sd_blob_duration = event['properties']['sd_blob_duration']\n            self.assertFalse(event['properties']['added_fixed_peers'])\n            self.assertEqual(event['properties']['wallet_server'], \"fakespv.lbry.com:50001\")\n            self.assertGreaterEqual(total_duration, resolve_duration + head_blob_duration + sd_blob_duration)\n\n        await self._test_time_to_first_bytes(check_post)\n\n    async def test_fixed_peer_delay_dht_peers_found(self):\n        self.client_config.fixed_peers = [(self.server_from_client.address, self.server_from_client.tcp_port)]\n        server_from_client = None\n        self.server_from_client, server_from_client = server_from_client, self.server_from_client\n\n        def after_setup():\n            self.stream_manager.node.protocol.routing_table.get_peers = lambda: [server_from_client]\n\n        def check_post(event):\n            self.assertEqual(event['event'], 'Time To First Bytes')\n            total_duration = event['properties']['total_duration']\n            resolve_duration = event['properties']['resolve_duration']\n            head_blob_duration = event['properties']['head_blob_duration']\n            sd_blob_duration = event['properties']['sd_blob_duration']\n\n            self.assertEqual(event['event'], 'Time To First Bytes')\n            self.assertEqual(event['properties']['tried_peers_count'], 1)\n            self.assertEqual(event['properties']['active_peer_count'], 1)\n            self.assertEqual(event['properties']['connection_failures_count'], 0)\n            self.assertTrue(event['properties']['use_fixed_peers'])\n            self.assertTrue(event['properties']['added_fixed_peers'])\n            self.assertEqual(event['properties']['fixed_peer_delay'], self.client_config.fixed_peer_delay)\n            self.assertGreaterEqual(total_duration, resolve_duration + head_blob_duration + sd_blob_duration)\n\n        await self._test_time_to_first_bytes(check_post, after_setup=after_setup)\n\n    async def test_tcp_connection_failure_analytics(self):\n        self.client_config.download_timeout = 3.0\n\n        def after_setup():\n            self.server.stop_server()\n\n        def check_post(event):\n            self.assertEqual(event['event'], 'Time To First Bytes')\n            self.assertIsNone(event['properties']['head_blob_duration'])\n            self.assertIsNone(event['properties']['sd_blob_duration'])\n            self.assertFalse(event['properties']['added_fixed_peers'])\n            self.assertEqual(event['properties']['connection_failures_count'],  1)\n            self.assertEqual(\n                event['properties']['error_message'], f'Failed to download sd blob {self.sd_hash} within timeout.'\n            )\n\n        await self._test_time_to_first_bytes(check_post, DownloadSDTimeoutError, after_setup=after_setup)\n\n    async def test_override_fixed_peer_delay_dht_disabled(self):\n        self.client_config.fixed_peers = [(self.server_from_client.address, self.server_from_client.tcp_port)]\n        self.client_config.components_to_skip = ['dht', 'hash_announcer']\n        self.client_config.fixed_peer_delay = 9001.0\n        self.server_from_client = None\n\n        def check_post(event):\n            total_duration = event['properties']['total_duration']\n            resolve_duration = event['properties']['resolve_duration']\n            head_blob_duration = event['properties']['head_blob_duration']\n            sd_blob_duration = event['properties']['sd_blob_duration']\n\n            self.assertEqual(event['event'], 'Time To First Bytes')\n            self.assertEqual(event['properties']['tried_peers_count'], 1)\n            self.assertEqual(event['properties']['active_peer_count'], 1)\n            self.assertTrue(event['properties']['use_fixed_peers'])\n            self.assertTrue(event['properties']['added_fixed_peers'])\n            self.assertEqual(event['properties']['fixed_peer_delay'], 0.0)\n            self.assertGreaterEqual(total_duration, resolve_duration + head_blob_duration + sd_blob_duration)\n\n        start = self.loop.time()\n        await self._test_time_to_first_bytes(check_post)\n        self.assertLess(self.loop.time() - start, 3)\n\n    async def test_no_peers_timeout(self):\n        # FIXME: the download should ideally fail right away if there are no peers\n        # to initialize the shortlist and fixed peers are disabled\n        self.server_from_client = None\n        self.client_config.download_timeout = 3.0\n\n        def check_post(event):\n            self.assertEqual(event['event'], 'Time To First Bytes')\n            self.assertEqual(event['properties']['error'], 'DownloadSDTimeoutError')\n            self.assertEqual(event['properties']['tried_peers_count'], 0)\n            self.assertEqual(event['properties']['active_peer_count'], 0)\n            self.assertFalse(event['properties']['use_fixed_peers'])\n            self.assertFalse(event['properties']['added_fixed_peers'])\n            self.assertIsNone(event['properties']['fixed_peer_delay'])\n            self.assertEqual(\n                event['properties']['error_message'], f'Failed to download sd blob {self.sd_hash} within timeout.'\n            )\n\n        start = self.loop.time()\n        await self._test_time_to_first_bytes(check_post, DownloadSDTimeoutError)\n        duration = self.loop.time() - start\n        self.assertLessEqual(duration, 5)\n        self.assertGreaterEqual(duration, 3.0)\n\n    async def test_download_stop_resume_delete(self):\n        await self.setup_stream_manager()\n        received = []\n        expected_events = ['Time To First Bytes', 'Download Finished']\n\n        async def check_post(event):\n            received.append(event['event'])\n\n        self.stream_manager.analytics_manager._post = check_post\n\n        self.assertDictEqual(self.stream_manager.streams, {})\n        stream = await self.file_manager.download_from_uri(self.uri, self.exchange_rate_manager)\n        stream_hash = stream.stream_hash\n        self.assertDictEqual(self.stream_manager.streams, {stream.sd_hash: stream})\n        self.assertTrue(stream.running)\n        self.assertFalse(stream.finished)\n        self.assertTrue(os.path.isfile(os.path.join(self.client_dir, \"test_file\")))\n        stored_status = await self.client_storage.run_and_return_one_or_none(\n            \"select status from file where stream_hash=?\", stream_hash\n        )\n        self.assertEqual(stored_status, \"running\")\n\n        await stream.stop()\n\n        self.assertFalse(stream.finished)\n        self.assertFalse(stream.running)\n        self.assertFalse(os.path.isfile(os.path.join(self.client_dir, \"test_file\")))\n        stored_status = await self.client_storage.run_and_return_one_or_none(\n            \"select status from file where stream_hash=?\", stream_hash\n        )\n        self.assertEqual(stored_status, \"stopped\")\n\n        stream.downloader.node = self.stream_manager.node\n        await stream.save_file()\n        await stream.finished_writing.wait()\n        await asyncio.sleep(0)\n        self.assertTrue(stream.finished)\n        self.assertFalse(stream.running)\n        self.assertTrue(os.path.isfile(os.path.join(self.client_dir, \"test_file\")))\n        stored_status = await self.client_storage.run_and_return_one_or_none(\n            \"select status from file where stream_hash=?\", stream_hash\n        )\n        self.assertEqual(stored_status, \"finished\")\n\n        await self.stream_manager.delete(stream, True)\n        self.assertDictEqual(self.stream_manager.streams, {})\n        self.assertFalse(os.path.isfile(os.path.join(self.client_dir, \"test_file\")))\n        stored_status = await self.client_storage.run_and_return_one_or_none(\n            \"select status from file where stream_hash=?\", stream_hash\n        )\n        self.assertIsNone(stored_status)\n        self.assertListEqual(expected_events, received)\n\n    async def _test_download_error_on_start(self, expected_error, timeout=None):\n        error = None\n        try:\n            await self.file_manager.download_from_uri(self.uri, self.exchange_rate_manager, timeout)\n        except Exception as err:\n            if isinstance(err, asyncio.CancelledError):  # TODO: remove when updated to 3.8\n                raise\n            error = err\n        self.assertEqual(expected_error, type(error))\n\n    async def _test_download_error_analytics_on_start(self, expected_error, error_message, timeout=None):\n        received = []\n\n        async def check_post(event):\n            self.assertEqual(\"Time To First Bytes\", event['event'])\n            self.assertEqual(event['properties']['error_message'], error_message)\n            received.append(event['properties']['error'])\n\n        self.stream_manager.analytics_manager._post = check_post\n        await self._test_download_error_on_start(expected_error, timeout)\n        await asyncio.sleep(0)\n        self.assertListEqual([expected_error.__name__], received)\n\n    async def test_insufficient_funds(self):\n        fee = {\n            'currency': 'LBC',\n            'amount': 11.0,\n            'address': 'bYFeMtSL7ARuG1iMpjFyrnTe4oJHSAVNXF',\n            'version': '_0_0_1'\n        }\n        await self.setup_stream_manager(10.0, fee)\n        await self._test_download_error_on_start(InsufficientFundsError, \"\")\n\n    async def test_fee_above_max_allowed(self):\n        fee = {\n            'currency': 'USD',\n            'amount': 51.0,\n            'address': 'bYFeMtSL7ARuG1iMpjFyrnTe4oJHSAVNXF',\n            'version': '_0_0_1'\n        }\n        await self.setup_stream_manager(1000000.0, fee)\n        await self._test_download_error_on_start(KeyFeeAboveMaxAllowedError, \"\")\n\n    async def test_resolve_error(self):\n        await self.setup_stream_manager()\n        self.uri = \"fake\"\n        await self._test_download_error_on_start(ResolveError)\n\n    async def test_download_sd_timeout(self):\n        self.server.stop_server()\n        await self.setup_stream_manager()\n        await self._test_download_error_analytics_on_start(\n            DownloadSDTimeoutError, f'Failed to download sd blob {self.sd_hash} within timeout.', timeout=1\n        )\n\n    async def test_download_data_timeout(self):\n        await self.setup_stream_manager()\n        with open(os.path.join(self.server_dir, self.sd_hash), 'r') as sdf:\n            head_blob_hash = json.loads(sdf.read())['blobs'][0]['blob_hash']\n        self.server_blob_manager.delete_blob(head_blob_hash)\n        await self._test_download_error_analytics_on_start(\n            DownloadDataTimeoutError, f'Failed to download data blobs for sd hash {self.sd_hash} within timeout.', timeout=1\n        )\n\n    async def test_unexpected_error(self):\n        await self.setup_stream_manager()\n        err_msg = f\"invalid blob directory '{self.client_dir}'\"\n        shutil.rmtree(self.client_dir)\n        await self._test_download_error_analytics_on_start(\n            OSError, err_msg, timeout=1\n        )\n        os.mkdir(self.client_dir)  # so the test cleanup doesn't error\n\n    async def test_non_head_data_timeout(self):\n        await self.setup_stream_manager()\n        with open(os.path.join(self.server_dir, self.sd_hash), 'r') as sdf:\n            last_blob_hash = json.loads(sdf.read())['blobs'][-2]['blob_hash']\n        self.server_blob_manager.delete_blob(last_blob_hash)\n        self.client_config.blob_download_timeout = 0.1\n        stream = await self.file_manager.download_from_uri(self.uri, self.exchange_rate_manager)\n        await stream.started_writing.wait()\n        self.assertEqual('running', stream.status)\n        self.assertIsNotNone(stream.full_path)\n        self.assertGreater(stream.written_bytes, 0)\n        await stream.finished_write_attempt.wait()\n        self.assertEqual('stopped', stream.status)\n        self.assertIsNone(stream.full_path)\n        self.assertEqual(0, stream.written_bytes)\n\n        await self.stream_manager.stop()\n        await self.stream_manager.start()\n        self.assertEqual(1, len(self.stream_manager.streams))\n        stream = list(self.stream_manager.streams.values())[0]\n        self.assertEqual('stopped', stream.status)\n        self.assertIsNone(stream.full_path)\n        self.assertEqual(0, stream.written_bytes)\n\n    async def test_download_then_recover_stream_on_startup(self, old_sort=False):\n        expected_analytics_events = [\n            'Time To First Bytes',\n            'Download Finished'\n        ]\n        received_events = []\n\n        async def check_post(event):\n            received_events.append(event['event'])\n\n        await self.setup_stream_manager(old_sort=old_sort)\n        self.stream_manager.analytics_manager._post = check_post\n\n        self.assertDictEqual(self.stream_manager.streams, {})\n        stream = await self.file_manager.download_from_uri(self.uri, self.exchange_rate_manager)\n        await stream.finished_writing.wait()\n        await asyncio.sleep(0)\n        await self.stream_manager.stop()\n        self.client_blob_manager.stop()\n        # partial removal, only sd blob is missing.\n        # in this case, we recover the sd blob while the other blobs are kept untouched as 'finished'\n        os.remove(os.path.join(self.client_blob_manager.blob_dir, stream.sd_hash))\n        await self.client_blob_manager.setup()\n        await self.stream_manager.start()\n        self.assertEqual(1, len(self.stream_manager.streams))\n        self.assertListEqual([self.sd_hash], list(self.stream_manager.streams.keys()))\n        for blob_hash in [stream.sd_hash] + [b.blob_hash for b in stream.descriptor.blobs[:-1]]:\n            blob_status = await self.client_storage.get_blob_status(blob_hash)\n            self.assertEqual('finished', blob_status)\n        self.assertEqual('finished', self.stream_manager.streams[self.sd_hash].status)\n\n        sd_blob = self.client_blob_manager.get_blob(stream.sd_hash)\n        self.assertTrue(sd_blob.file_exists)\n        self.assertTrue(sd_blob.get_is_verified())\n        self.assertListEqual(expected_analytics_events, received_events)\n\n        # full removal, check that status is preserved (except sd blob, which was written)\n        self.client_blob_manager.stop()\n        os.remove(os.path.join(self.client_blob_manager.blob_dir, stream.sd_hash))\n        for blob in stream.descriptor.blobs[:-1]:\n            os.remove(os.path.join(self.client_blob_manager.blob_dir, blob.blob_hash))\n        await self.client_blob_manager.setup()\n        await self.stream_manager.start()\n        for blob_hash in [b.blob_hash for b in stream.descriptor.blobs[:-1]]:\n            blob_status = await self.client_storage.get_blob_status(blob_hash)\n            self.assertEqual('pending', blob_status)\n        # sd blob was recovered\n        sd_blob = self.client_blob_manager.get_blob(stream.sd_hash)\n        self.assertTrue(sd_blob.file_exists)\n        self.assertTrue(sd_blob.get_is_verified())\n        self.assertListEqual(expected_analytics_events, received_events)\n        # db reflects that too\n        blob_status = await self.client_storage.get_blob_status(stream.sd_hash)\n        self.assertEqual('finished', blob_status)\n\n    def test_download_then_recover_old_sort_stream_on_startup(self):\n        return self.test_download_then_recover_stream_on_startup(old_sort=True)\n"
  },
  {
    "path": "tests/unit/test_cli.py",
    "content": "import os\nimport tempfile\nimport shutil\nimport contextlib\nimport logging\nimport pathlib\nfrom io import StringIO\nfrom unittest import TestCase\nfrom unittest.mock import patch\nfrom types import SimpleNamespace\nfrom contextlib import asynccontextmanager\n\nimport docopt\nfrom lbry.testcase import AsyncioTestCase\n\nfrom lbry.extras.cli import normalize_value, main, setup_logging, ensure_directory_exists\nfrom lbry.extras.system_info import get_platform\nfrom lbry.extras.daemon.daemon import Daemon\nfrom lbry.conf import Config\nfrom lbry.extras import cli\n\n\n@asynccontextmanager\nasync def get_logger(argv, **conf_options):\n    # loggly requires loop, so we do this in async function\n\n    logger = logging.getLogger('test-root-logger')\n    temp_dir = tempfile.mkdtemp()\n    temp_config = os.path.join(temp_dir, 'settings.yml')\n\n    try:\n        # create a config (to be loaded on startup)\n        _conf = Config.create_from_arguments(SimpleNamespace(config=temp_config))\n        with _conf.update_config():\n            for opt_name, opt_value in conf_options.items():\n                setattr(_conf, opt_name, opt_value)\n\n        # do what happens on startup\n        argv.extend(['--data-dir', temp_dir])\n        argv.extend(['--wallet-dir', temp_dir])\n        argv.extend(['--config', temp_config])\n        parser = cli.get_argument_parser()\n        args, command_args = parser.parse_known_args(argv)\n        conf: Config = Config.create_from_arguments(args)\n        setup_logging(logger, args, conf)\n        yield logger\n\n    finally:\n        shutil.rmtree(temp_dir, ignore_errors=True)\n        for mod in cli.LOG_MODULES:\n            log = logger.getChild(mod)\n            log.setLevel(logging.NOTSET)\n            while log.handlers:\n                h = log.handlers[0]\n                log.removeHandler(log.handlers[0])\n                h.close()\n\n\nclass CLILoggingTest(AsyncioTestCase):\n\n    async def test_verbose_logging(self):\n        async with get_logger([\"start\", \"--quiet\"], share_usage_data=False) as log:\n            log = log.getChild(\"lbry\")\n            self.assertTrue(log.isEnabledFor(logging.INFO))\n            self.assertFalse(log.isEnabledFor(logging.DEBUG))\n            self.assertEqual(len(log.handlers), 1)\n            self.assertIsInstance(log.handlers[0], logging.handlers.RotatingFileHandler)\n\n        async with get_logger([\"start\", \"--verbose\"]) as log:\n            self.assertTrue(log.getChild(\"lbry\").isEnabledFor(logging.DEBUG))\n            self.assertTrue(log.getChild(\"lbry\").isEnabledFor(logging.INFO))\n            self.assertFalse(log.getChild(\"torba\").isEnabledFor(logging.DEBUG))\n\n        async with get_logger([\"start\", \"--verbose\", \"lbry.extras\", \"lbry.wallet\", \"torba.client\"]) as log:\n            self.assertTrue(log.getChild(\"lbry.extras\").isEnabledFor(logging.DEBUG))\n            self.assertTrue(log.getChild(\"lbry.wallet\").isEnabledFor(logging.DEBUG))\n            self.assertTrue(log.getChild(\"torba.client\").isEnabledFor(logging.DEBUG))\n            self.assertFalse(log.getChild(\"lbry\").isEnabledFor(logging.DEBUG))\n            self.assertFalse(log.getChild(\"torba\").isEnabledFor(logging.DEBUG))\n\n    async def test_quiet(self):\n        async with get_logger([\"start\"]) as log:  # default is loud\n            log = log.getChild(\"lbry\")\n            self.assertEqual(len(log.handlers), 2)\n            self.assertIs(type(log.handlers[1]), logging.StreamHandler)\n        async with get_logger([\"start\", \"--quiet\"]) as log:\n            log = log.getChild(\"lbry\")\n            self.assertEqual(len(log.handlers), 1)\n            self.assertIsNot(type(log.handlers[0]), logging.StreamHandler)\n\n\nclass CLITest(AsyncioTestCase):\n\n    @staticmethod\n    def shell(argv):\n        actual_output = StringIO()\n        with contextlib.redirect_stdout(actual_output):\n            with contextlib.redirect_stderr(actual_output):\n                try:\n                    main(argv)\n                except SystemExit as e:\n                    print(e.args[0])\n        return actual_output.getvalue().strip()\n\n    def test_guess_type(self):\n        self.assertEqual('0.3.8', normalize_value('0.3.8'))\n        self.assertEqual('0.3', normalize_value('0.3'))\n        self.assertEqual(3, normalize_value('3'))\n        self.assertEqual(3, normalize_value(3))\n\n        self.assertEqual(\n            'VdNmakxFORPSyfCprAD/eDDPk5TY9QYtSA==',\n            normalize_value('VdNmakxFORPSyfCprAD/eDDPk5TY9QYtSA==')\n        )\n\n        self.assertTrue(normalize_value('TRUE'))\n        self.assertTrue(normalize_value('true'))\n        self.assertTrue(normalize_value('TrUe'))\n        self.assertFalse(normalize_value('FALSE'))\n        self.assertFalse(normalize_value('false'))\n        self.assertFalse(normalize_value('FaLsE'))\n        self.assertTrue(normalize_value(True))\n\n        self.assertEqual('3', normalize_value('3', key=\"uri\"))\n        self.assertEqual('0.3', normalize_value('0.3', key=\"uri\"))\n        self.assertEqual('True', normalize_value('True', key=\"uri\"))\n        self.assertEqual('False', normalize_value('False', key=\"uri\"))\n\n        self.assertEqual('3', normalize_value('3', key=\"file_name\"))\n        self.assertEqual('3', normalize_value('3', key=\"name\"))\n        self.assertEqual('3', normalize_value('3', key=\"download_directory\"))\n        self.assertEqual('3', normalize_value('3', key=\"channel_name\"))\n        self.assertEqual('3', normalize_value('3', key=\"claim_name\"))\n\n        self.assertEqual(3, normalize_value('3', key=\"some_other_thing\"))\n\n    def test_help(self):\n        self.assertIn('lbrynet [-v] [--api HOST:PORT]', self.shell(['--help']))\n        # start is special command, with separate help handling\n        self.assertIn('--share-usage-data', self.shell(['start', '--help']))\n        # publish is ungrouped command, returns usage only implicitly\n        self.assertIn('publish (<name> | --name=<name>)', self.shell(['publish']))\n        # publish is ungrouped command, with explicit --help\n        self.assertIn('Create or replace a stream claim at a given name', self.shell(['publish', '--help']))\n        # account is a group, returns help implicitly\n        self.assertIn('Return the balance of an account', self.shell(['account']))\n        # account is a group, with explicit --help\n        self.assertIn('Return the balance of an account', self.shell(['account', '--help']))\n        # account add is a grouped command, returns usage implicitly\n        self.assertIn('account_add (<account_name> | --account_name=<account_name>)', self.shell(['account', 'add']))\n        # account add is a grouped command, with explicit --help\n        self.assertIn('Add a previously created account from a seed,', self.shell(['account', 'add', '--help']))\n\n    def test_help_error_handling(self):\n        # person tries `help` command, then they get help even though that's invalid command\n        self.assertIn('--config FILE', self.shell(['help']))\n        # help for invalid command, with explicit --help\n        self.assertIn('--config FILE', self.shell(['nonexistant', '--help']))\n        # help for invalid command, implicit\n        self.assertIn('--config FILE', self.shell(['nonexistant']))\n\n    def test_version_command(self):\n        self.assertEqual(\n            \"lbrynet {lbrynet_version}\".format(**get_platform()), self.shell(['--version'])\n        )\n\n    def test_valid_command_daemon_not_started(self):\n        self.assertEqual(\n            \"Could not connect to daemon. Are you sure it's running?\",\n            self.shell([\"publish\", 'asd'])\n        )\n\n    def test_deprecated_command_daemon_not_started(self):\n        actual_output = StringIO()\n        with contextlib.redirect_stdout(actual_output):\n            main([\"channel\", \"new\", \"@foo\", \"1.0\"])\n        self.assertEqual(\n            actual_output.getvalue().strip(),\n            \"channel_new is deprecated, using channel_create.\\n\"\n            \"Could not connect to daemon. Are you sure it's running?\"\n        )\n\n    @patch.object(Daemon, 'start', spec=Daemon, wraps=Daemon.start)\n    def test_keyboard_interrupt_handling(self, mock_daemon_start):\n        def side_effect():\n            raise KeyboardInterrupt\n\n        mock_daemon_start.side_effect = side_effect\n        self.shell([\"start\", \"--no-logging\"])\n        mock_daemon_start.assert_called_once()\n\n\nclass DaemonDocsTests(TestCase):\n\n    def test_can_parse_api_method_docs(self):\n        failures = []\n        for name, fn in Daemon.callable_methods.items():\n            try:\n                docopt.docopt(fn.__doc__, ())\n            except docopt.DocoptLanguageError as err:\n                failures.append(f\"invalid docstring for {name}, {err.args[0]}\")\n            except docopt.DocoptExit:\n                pass\n        if failures:\n            self.fail(\"\\n\" + \"\\n\".join(failures))\n\n\nclass EnsureDirectoryExistsTests(TestCase):\n\n    def setUp(self):\n        self.temp_dir = tempfile.mkdtemp()\n\n    def tearDown(self):\n        shutil.rmtree(self.temp_dir)\n\n    def test_when_parent_dir_does_not_exist_then_dir_is_created_with_parent(self):\n        dir_path = os.path.join(self.temp_dir, \"parent_dir\", \"dir\")\n        ensure_directory_exists(dir_path)\n        self.assertTrue(os.path.exists(dir_path))\n\n    def test_when_non_writable_dir_exists_then_raise(self):\n        dir_path = os.path.join(self.temp_dir, \"dir\")\n        pathlib.Path(dir_path).mkdir(mode=0o555)  # creates a non-writable, readable and executable dir\n        with self.assertRaises(PermissionError):\n            ensure_directory_exists(dir_path)\n\n    def test_when_dir_exists_and_writable_then_no_raise(self):\n        dir_path = os.path.join(self.temp_dir, \"dir\")\n        pathlib.Path(dir_path).mkdir(mode=0o777)  # creates a writable, readable and executable dir\n        try:\n            ensure_directory_exists(dir_path)\n        except (FileExistsError, PermissionError) as err:\n            self.fail(f\"{type(err).__name__} was raised\")\n\n    def test_when_non_dir_file_exists_at_path_then_raise(self):\n        file_path = os.path.join(self.temp_dir, \"file.extension\")\n        pathlib.Path(file_path).touch()\n        with self.assertRaises(FileExistsError):\n            ensure_directory_exists(file_path)\n"
  },
  {
    "path": "tests/unit/test_conf.py",
    "content": "import os\nimport sys\nimport types\nimport tempfile\nimport unittest\nimport argparse\nimport lbry.wallet\nfrom lbry.conf import Config, BaseConfig, String, Integer, Toggle, Servers, Strings, StringChoice, NOT_SET\nfrom lbry.error import InvalidCurrencyError\n\n\nclass TestConfig(BaseConfig):\n    test_str = String('str help', 'the default', previous_names=['old_str'])\n    test_int = Integer('int help', 9)\n    test_false_toggle = Toggle('toggle help', False)\n    test_true_toggle = Toggle('toggle help', True)\n    servers = Servers('servers help', [('localhost', 80)])\n    strings = Strings('cheese', ['string'])\n    string_choice = StringChoice(\"one of string\", [\"a\", \"b\", \"c\"], \"a\")\n\n\nclass ConfigurationTests(unittest.TestCase):\n\n    @unittest.skipIf('darwin' not in sys.platform, 'skipping mac only test')\n    def test_mac_defaults(self):\n        c = Config()\n        self.assertEqual(c.data_dir, os.path.expanduser(\"~/Library/Application Support/LBRY\"))\n        self.assertEqual(c.wallet_dir, os.path.expanduser('~/.lbryum'))\n        self.assertEqual(c.download_dir, os.path.expanduser('~/Downloads'))\n        self.assertEqual(c.config, os.path.join(c.data_dir, 'daemon_settings.yml'))\n        self.assertEqual(c.api_connection_url, 'http://localhost:5279/lbryapi')\n        self.assertEqual(c.log_file_path, os.path.join(c.data_dir, 'lbrynet.log'))\n\n    @unittest.skipIf('win32' not in sys.platform, 'skipping windows only test')\n    def test_windows_defaults(self):\n        c = Config()\n        prefix = os.path.join(r\"C:\\Users\", os.getlogin(), r\"AppData\\Local\\lbry\")\n        self.assertEqual(c.data_dir, os.path.join(prefix, 'lbrynet'))\n        self.assertEqual(c.wallet_dir, os.path.join(prefix, 'lbryum'))\n        self.assertEqual(c.download_dir, os.path.join(r\"C:\\Users\", os.getlogin(), \"Downloads\"))\n        self.assertEqual(c.config, os.path.join(c.data_dir, 'daemon_settings.yml'))\n        self.assertEqual(c.api_connection_url, 'http://localhost:5279/lbryapi')\n        self.assertEqual(c.log_file_path, os.path.join(c.data_dir, 'lbrynet.log'))\n\n    @unittest.skipIf('linux' not in sys.platform, 'skipping linux only test')\n    def test_linux_defaults(self):\n        c = Config()\n        self.assertEqual(c.data_dir, os.path.expanduser('~/.local/share/lbry/lbrynet'))\n        self.assertEqual(c.wallet_dir, os.path.expanduser('~/.local/share/lbry/lbryum'))\n        self.assertEqual(c.download_dir, os.path.expanduser('~/Downloads'))\n        self.assertEqual(c.config, os.path.expanduser('~/.local/share/lbry/lbrynet/daemon_settings.yml'))\n        self.assertEqual(c.api_connection_url, 'http://localhost:5279/lbryapi')\n        self.assertEqual(c.log_file_path, os.path.expanduser('~/.local/share/lbry/lbrynet/lbrynet.log'))\n\n    def test_search_order(self):\n        c = TestConfig()\n        c.runtime = {'test_str': 'runtime'}\n        c.arguments = {'test_str': 'arguments'}\n        c.environment = {'test_str': 'environment'}\n        c.persisted = {'test_str': 'persisted'}\n        self.assertEqual(c.test_str, 'runtime')\n        c.runtime = {}\n        self.assertEqual(c.test_str, 'arguments')\n        c.arguments = {}\n        self.assertEqual(c.test_str, 'environment')\n        c.environment = {}\n        self.assertEqual(c.test_str, 'persisted')\n        c.persisted = {}\n        self.assertEqual(c.test_str, 'the default')\n\n    def test_is_set(self):\n        c = TestConfig()\n        self.assertEqual(c.test_str, 'the default')\n        self.assertFalse(TestConfig.test_str.is_set(c))\n        c.test_str = 'new value'\n        self.assertEqual(c.test_str, 'new value')\n        self.assertTrue(TestConfig.test_str.is_set(c))\n\n    def test_is_set_to_default(self):\n        c = TestConfig()\n        self.assertEqual(TestConfig.test_str.default, 'the default')\n        self.assertFalse(TestConfig.test_str.is_set(c))\n        self.assertFalse(TestConfig.test_str.is_set_to_default(c))\n        c.test_str = 'new value'\n        self.assertTrue(TestConfig.test_str.is_set(c))\n        self.assertFalse(TestConfig.test_str.is_set_to_default(c))\n        c.test_str = 'the default'\n        self.assertTrue(TestConfig.test_str.is_set(c))\n        self.assertTrue(TestConfig.test_str.is_set_to_default(c))\n\n    def test_arguments(self):\n        parser = argparse.ArgumentParser()\n        TestConfig.contribute_to_argparse(parser)\n\n        args = parser.parse_args([])\n        c = TestConfig.create_from_arguments(args)\n        self.assertEqual(c.test_str, 'the default')\n        self.assertTrue(c.test_true_toggle)\n        self.assertFalse(c.test_false_toggle)\n        self.assertEqual(c.servers, [('localhost', 80)])\n        self.assertEqual(c.strings, ['string'])\n\n        args = parser.parse_args(['--test-str', 'blah'])\n        c = TestConfig.create_from_arguments(args)\n        self.assertEqual(c.test_str, 'blah')\n        self.assertTrue(c.test_true_toggle)\n        self.assertFalse(c.test_false_toggle)\n\n        args = parser.parse_args(['--test-true-toggle'])\n        c = TestConfig.create_from_arguments(args)\n        self.assertTrue(c.test_true_toggle)\n        self.assertFalse(c.test_false_toggle)\n\n        args = parser.parse_args(['--test-false-toggle'])\n        c = TestConfig.create_from_arguments(args)\n        self.assertTrue(c.test_true_toggle)\n        self.assertTrue(c.test_false_toggle)\n\n        args = parser.parse_args(['--no-test-true-toggle'])\n        c = TestConfig.create_from_arguments(args)\n        self.assertFalse(c.test_true_toggle)\n        self.assertFalse(c.test_false_toggle)\n\n        args = parser.parse_args(['--servers=localhost:1', '--servers=192.168.0.1:2'])\n        c = TestConfig.create_from_arguments(args)\n        self.assertEqual(c.servers, [('localhost', 1), ('192.168.0.1', 2)])\n\n        args = parser.parse_args(['--strings=cheddar', '--strings=mozzarella'])\n        c = TestConfig.create_from_arguments(args)\n        self.assertEqual(c.strings, ['cheddar', 'mozzarella'])\n\n    def test_environment(self):\n        c = TestConfig()\n\n        self.assertEqual(c.test_str, 'the default')\n        c.set_environment({'LBRY_TEST_STR': 'from environ'})\n        self.assertEqual(c.test_str, 'from environ')\n\n        self.assertEqual(c.test_int, 9)\n        c.set_environment({'LBRY_TEST_INT': '1'})\n        self.assertEqual(c.test_int, 1)\n\n    def test_persisted(self):\n        with tempfile.TemporaryDirectory() as temp_dir:\n\n            c = TestConfig.create_from_arguments(\n                types.SimpleNamespace(config=os.path.join(temp_dir, 'settings.yml'))\n            )\n\n            # settings.yml doesn't exist on file system\n            self.assertFalse(c.persisted.exists)\n            self.assertEqual(c.test_str, 'the default')\n\n            self.assertEqual(c.modify_order, [c.runtime])\n            with c.update_config():\n                self.assertEqual(c.modify_order, [c.runtime, c.persisted])\n                c.test_str = 'original'\n            self.assertEqual(c.modify_order, [c.runtime])\n\n            # share_usage_data has been saved to settings file\n            self.assertTrue(c.persisted.exists)\n            with open(c.config, 'r') as fd:\n                self.assertEqual(fd.read(), 'test_str: original\\n')\n\n            # load the settings file and check share_usage_data is false\n            c = TestConfig.create_from_arguments(\n                types.SimpleNamespace(config=os.path.join(temp_dir, 'settings.yml'))\n            )\n            self.assertTrue(c.persisted.exists)\n            self.assertEqual(c.test_str, 'original')\n\n            # setting in runtime overrides config\n            self.assertNotIn('test_str', c.runtime)\n            c.test_str = 'from runtime'\n            self.assertIn('test_str', c.runtime)\n            self.assertEqual(c.test_str, 'from runtime')\n\n            # without context manager NOT_SET only clears it in runtime location\n            c.test_str = NOT_SET\n            self.assertNotIn('test_str', c.runtime)\n            self.assertEqual(c.test_str, 'original')\n\n            # clear it in persisted as well by using context manager\n            self.assertIn('test_str', c.persisted)\n            with c.update_config():\n                c.test_str = NOT_SET\n            self.assertNotIn('test_str', c.persisted)\n            self.assertEqual(c.test_str, 'the default')\n            with open(c.config, 'r') as fd:\n                self.assertEqual(fd.read(), '{}\\n')\n\n    def test_persisted_upgrade(self):\n        with tempfile.TemporaryDirectory() as temp_dir:\n            config = os.path.join(temp_dir, 'settings.yml')\n            with open(config, 'w') as fd:\n                fd.write('old_str: old stuff\\n')\n            c = TestConfig.create_from_arguments(\n                types.SimpleNamespace(config=config)\n            )\n            self.assertEqual(c.test_str, 'old stuff')\n            self.assertNotIn('old_str', c.persisted)\n            with open(config, 'w') as fd:\n                fd.write('test_str: old stuff\\n')\n\n    def test_validation(self):\n        c = TestConfig()\n        with self.assertRaisesRegex(AssertionError, 'must be a string'):\n            c.test_str = 9\n        with self.assertRaisesRegex(AssertionError, 'must be an integer'):\n            c.test_int = 'hi'\n        with self.assertRaisesRegex(AssertionError, 'must be a true/false'):\n            c.test_true_toggle = 'hi'\n            c.test_false_toggle = 'hi'\n\n    def test_file_extension_validation(self):\n        with self.assertRaisesRegex(AssertionError, \"'.json' is not supported\"):\n            TestConfig.create_from_arguments(\n                types.SimpleNamespace(config=os.path.join('settings.json'))\n            )\n\n    def test_serialize_deserialize(self):\n        with tempfile.TemporaryDirectory() as temp_dir:\n            c = TestConfig.create_from_arguments(\n                types.SimpleNamespace(config=os.path.join(temp_dir, 'settings.yml'))\n            )\n            self.assertEqual(c.servers, [('localhost', 80)])\n            with c.update_config():\n                c.servers = [('localhost', 8080)]\n            with open(c.config, 'r+') as fd:\n                self.assertEqual(fd.read(), 'servers:\\n- localhost:8080\\n')\n                fd.write('servers:\\n  - localhost:5566\\n')\n            c = TestConfig.create_from_arguments(\n                types.SimpleNamespace(config=os.path.join(temp_dir, 'settings.yml'))\n            )\n            self.assertEqual(c.servers, [('localhost', 5566)])\n\n    def test_max_key_fee_from_yaml(self):\n        with tempfile.TemporaryDirectory() as temp_dir:\n            config = os.path.join(temp_dir, 'settings.yml')\n            with open(config, 'w') as fd:\n                fd.write('max_key_fee: {currency: USD, amount: 1}\\n')\n            c = Config.create_from_arguments(\n                types.SimpleNamespace(config=config)\n            )\n            self.assertEqual(c.max_key_fee['currency'], 'USD')\n            self.assertEqual(c.max_key_fee['amount'], 1)\n            with self.assertRaises(InvalidCurrencyError):\n                c.max_key_fee = {'currency': 'BCH', 'amount': 1}\n            with c.update_config():\n                c.max_key_fee = {'currency': 'BTC', 'amount': 1}\n            with open(config, 'r') as fd:\n                self.assertEqual(fd.read(), 'max_key_fee:\\n  amount: 1\\n  currency: BTC\\n')\n            with c.update_config():\n                c.max_key_fee = None\n            with open(config, 'r') as fd:\n                self.assertEqual(fd.read(), 'max_key_fee: null\\n')\n\n    def test_max_key_fee_from_args(self):\n        parser = argparse.ArgumentParser()\n        Config.contribute_to_argparse(parser)\n\n        # default\n        args = parser.parse_args([])\n        c = Config.create_from_arguments(args)\n        self.assertEqual(c.max_key_fee, {'amount': 50.0, 'currency': 'USD'})\n\n        # disabled\n        args = parser.parse_args(['--no-max-key-fee'])\n        c = Config.create_from_arguments(args)\n        self.assertIsNone(c.max_key_fee)\n\n        args = parser.parse_args(['--max-key-fee', 'null'])\n        c = Config.create_from_arguments(args)\n        self.assertIsNone(c.max_key_fee)\n\n        # set\n        args = parser.parse_args(['--max-key-fee', '1.0', 'BTC'])\n        c = Config.create_from_arguments(args)\n        self.assertEqual(c.max_key_fee, {'amount': 1.0, 'currency': 'BTC'})\n\n    def test_string_choice(self):\n        with self.assertRaisesRegex(ValueError, \"No valid values provided\"):\n            StringChoice(\"no valid values\", [], \"\")\n        with self.assertRaisesRegex(ValueError, \"Default value must be one of\"):\n            StringChoice(\"invalid default\", [\"a\"], \"b\")\n\n        c = TestConfig()\n        self.assertEqual(\"a\", c.string_choice)  # default\n        c.string_choice = \"b\"\n        self.assertEqual(\"b\", c.string_choice)\n        with self.assertRaisesRegex(ValueError, \"Setting 'string_choice' value must be one of\"):\n            c.string_choice = \"d\"\n\n        parser = argparse.ArgumentParser()\n        TestConfig.contribute_to_argparse(parser)\n        args = parser.parse_args(['--string-choice', 'c'])\n        c = TestConfig.create_from_arguments(args)\n        self.assertEqual(\"c\", c.string_choice)\n\n    def test_known_hubs_list(self):\n        with tempfile.TemporaryDirectory() as temp_dir:\n            hubs = Config(config=os.path.join(temp_dir, 'settings.yml'), wallet_dir=temp_dir).known_hubs\n\n            self.assertEqual(hubs.serialized, {})\n            self.assertEqual(list(hubs), [])\n            self.assertFalse(hubs)\n            hubs.set('new.hub.io:99', {'jurisdiction': 'us'})\n            self.assertTrue(hubs)\n\n            self.assertFalse(hubs.exists)\n            hubs.save()\n            self.assertTrue(hubs.exists)\n\n            hubs = Config(config=os.path.join(temp_dir, 'settings.yml'), wallet_dir=temp_dir).known_hubs\n            self.assertEqual(list(hubs), [('new.hub.io', 99)])\n            self.assertEqual(hubs.serialized, {'new.hub.io:99': {'jurisdiction': 'us'}})\n\n            hubs.set('any.hub.io:99', {})\n            hubs.set('oth.hub.io:99', {'jurisdiction': 'other'})\n            self.assertEqual(list(hubs), [('new.hub.io', 99), ('any.hub.io', 99), ('oth.hub.io', 99)])\n            self.assertEqual(hubs.filter(), {\n                ('new.hub.io', 99): {'jurisdiction': 'us'},\n                ('oth.hub.io', 99): {'jurisdiction': 'other'},\n                ('any.hub.io', 99): {}\n            })\n            self.assertEqual(hubs.filter(foo=\"bar\"), {})\n            self.assertEqual(hubs.filter(jurisdiction=\"us\"), {\n                ('new.hub.io', 99): {'jurisdiction': 'us'}\n            })\n            self.assertEqual(hubs.filter(jurisdiction=\"us\", match_none=True), {\n                ('new.hub.io', 99): {'jurisdiction': 'us'},\n                ('any.hub.io', 99): {}\n            })\n"
  },
  {
    "path": "tests/unit/test_utils.py",
    "content": "import unittest\nfrom lbry import utils\n\n\nclass UtilsTestCase(unittest.TestCase):\n\n    def test_get_colliding_prefix_bits(self):\n        self.assertEqual(\n            0, utils.get_colliding_prefix_bits(0xffffffff.to_bytes(4, \"big\"), 0x0000000000.to_bytes(4, \"big\")))\n        self.assertEqual(\n            1, utils.get_colliding_prefix_bits(0x7fffffff.to_bytes(4, \"big\"), 0x0000000000.to_bytes(4, \"big\")))\n        self.assertEqual(\n            8, utils.get_colliding_prefix_bits(0x00ffffff.to_bytes(4, \"big\"), 0x0000000000.to_bytes(4, \"big\")))\n        self.assertEqual(\n            8, utils.get_colliding_prefix_bits(0x00ffffff.to_bytes(4, \"big\"), 0x0000000000.to_bytes(4, \"big\")))\n        self.assertEqual(\n            1, utils.get_colliding_prefix_bits(0x7fffffff.to_bytes(4, \"big\"), 0x0000000000.to_bytes(4, \"big\")))\n        self.assertEqual(\n            1, utils.get_colliding_prefix_bits(0x7fffffff.to_bytes(4, \"big\"), 0x0000000000.to_bytes(4, \"big\")))\n"
  },
  {
    "path": "tests/unit/torrent/__init__.py",
    "content": ""
  },
  {
    "path": "tests/unit/torrent/test_tracker.py",
    "content": "import asyncio\nimport random\n\nfrom lbry.testcase import AsyncioTestCase\nfrom lbry.dht.peer import KademliaPeer\nfrom lbry.torrent.tracker import CompactIPv4Peer, TrackerClient, enqueue_tracker_search, UDPTrackerServerProtocol, encode_peer\n\n\nclass UDPTrackerClientTestCase(AsyncioTestCase):\n    async def asyncSetUp(self):\n        self.client_servers_list = []\n        self.servers = {}\n        self.client = TrackerClient(b\"\\x00\" * 48, 4444, lambda: self.client_servers_list, timeout=1)\n        await self.client.start()\n        self.addCleanup(self.client.stop)\n        await self.add_server()\n\n    async def add_server(self, port=None, add_to_client=True):\n        port = port or len(self.servers) + 59990\n        assert port not in self.servers\n        server = UDPTrackerServerProtocol()\n        self.servers[port] = server\n        transport, _ = await self.loop.create_datagram_endpoint(lambda: server, local_addr=(\"127.0.0.1\", port))\n        self.addCleanup(transport.close)\n        if add_to_client:\n            self.client_servers_list.append((\"127.0.0.1\", port))\n\n    async def test_announce(self):\n        info_hash = random.getrandbits(160).to_bytes(20, \"big\", signed=False)\n        announcement = (await self.client.get_peer_list(info_hash))[0]\n        self.assertEqual(announcement.seeders, 1)\n        self.assertEqual(announcement.peers,\n                         [CompactIPv4Peer(int.from_bytes(bytes([127, 0, 0, 1]), \"big\", signed=False), 4444)])\n\n    async def test_announce_many_info_hashes_to_many_servers_with_bad_one_and_dns_error(self):\n        await asyncio.gather(*[self.add_server() for _ in range(3)])\n        self.client_servers_list.append((\"no.it.does.not.exist\", 7070))\n        self.client_servers_list.append((\"127.0.0.2\", 7070))\n        info_hashes = [random.getrandbits(160).to_bytes(20, \"big\", signed=False) for _ in range(5)]\n        await self.client.announce_many(*info_hashes)\n        for server in self.servers.values():\n            self.assertDictEqual(\n                server.peers, {\n                    info_hash: [encode_peer(\"127.0.0.1\", self.client.announce_port)] for info_hash in info_hashes\n            })\n\n    async def test_announce_using_helper_function(self):\n        info_hash = random.getrandbits(160).to_bytes(20, \"big\", signed=False)\n        queue = asyncio.Queue()\n        enqueue_tracker_search(info_hash, queue)\n        peers = await queue.get()\n        self.assertEqual(peers, [KademliaPeer('127.0.0.1', None, None, 4444, allow_localhost=True)])\n\n    async def test_error(self):\n        info_hash = random.getrandbits(160).to_bytes(20, \"big\", signed=False)\n        await self.client.get_peer_list(info_hash)\n        list(self.servers.values())[0].known_conns.clear()\n        self.client.results.clear()\n        with self.assertRaises(Exception) as err:\n            await self.client.get_peer_list(info_hash)\n        self.assertEqual(err.exception.args[0], b'Connection ID missmatch.\\x00')\n\n    async def test_multiple_servers(self):\n        await asyncio.gather(*[self.add_server() for _ in range(10)])\n        info_hash = random.getrandbits(160).to_bytes(20, \"big\", signed=False)\n        await self.client.get_peer_list(info_hash)\n        for server in self.servers.values():\n            self.assertEqual(server.peers, {info_hash: [encode_peer(\"127.0.0.1\", self.client.announce_port)]})\n\n    async def test_multiple_servers_with_bad_one(self):\n        await asyncio.gather(*[self.add_server() for _ in range(10)])\n        self.client_servers_list.append((\"127.0.0.2\", 7070))\n        info_hash = random.getrandbits(160).to_bytes(20, \"big\", signed=False)\n        await self.client.get_peer_list(info_hash)\n        for server in self.servers.values():\n            self.assertEqual(server.peers, {info_hash: [encode_peer(\"127.0.0.1\", self.client.announce_port)]})\n\n    async def test_multiple_servers_with_different_peers_across_helper_function(self):\n        # this is how the downloader uses it\n        await asyncio.gather(*[self.add_server() for _ in range(10)])\n        info_hash = random.getrandbits(160).to_bytes(20, \"big\", signed=False)\n        fake_peers = []\n        for server in self.servers.values():\n            for _ in range(10):\n                peer = (f\"127.0.0.{random.randint(1, 255)}\", random.randint(2000, 65500))\n                fake_peers.append(peer)\n                server.add_peer(info_hash, *peer)\n        peer_q = asyncio.Queue()\n        enqueue_tracker_search(info_hash, peer_q)\n        await asyncio.sleep(0)\n        await asyncio.gather(*self.client.tasks.values())\n        self.assertEqual(11, peer_q.qsize())\n"
  },
  {
    "path": "tests/unit/wallet/__init__.py",
    "content": ""
  },
  {
    "path": "tests/unit/wallet/key_fixtures.py",
    "content": "expected_ids = [\n    b'948adae2a128c0bd1fa238117fd0d9690961f26e',\n    b'cd9f4f2adde7de0a53ab6d326bb6a62b489876dd',\n    b'c479e02a74a809ffecff60255d1c14f4081a197a',\n    b'4bab2fb2c424f31f170b15ec53c4a596db9d6710',\n    b'689cb7c621f57b7c398e7e04ed9a5098ab8389e9',\n    b'75116d6a689a0f9b56fe7cfec9cbbd0e16814288',\n    b'2439f0993fb298497dd7f317b9737c356f664a86',\n    b'32f1cb4799008cf5496bb8cafdaf59d5dabec6af',\n    b'fa29aa536353904e9cc813b0cf18efcc09e5ad13',\n    b'37df34002f34d7875428a2977df19be3f4f40a31',\n    b'8c8a72b5d2747a3e7e05ed85110188769d5656c3',\n    b'e5c8ef10c5bdaa79c9a237a096f50df4dcac27f0',\n    b'4d5270dc100fba85974665c20cd0f95d4822e8d1',\n    b'e76b07da0cdd59915475cd310599544b9744fa34',\n    b'6f009bccf8be99707161abb279d8ccf8fd953721',\n    b'f32f08b722cc8607c3f7f192b4d5f13a74c85785',\n    b'46f4430a5c91b9b799e9be6b47ac7a749d8d9f30',\n    b'ebbf9850abe0aae2d09e7e3ebd6b51f01282f39b',\n    b'5f6655438f8ddc6b2f6ea8197c8babaffc9f5c09',\n    b'e194e70ee8711b0ed765608121e4cceb551cdf28'\n]\nexpected_privkeys = [\n    b'95557ee9a2bb7665e67e45246658b5c839f7dcd99b6ebc800eeebccd28bf134a',\n    b'689b6921f65647a8e4fc1497924730c92ad4ad183f10fac2bdee65cc8fb6dcf9',\n    b'977ee018b448c530327b7e927cc3645ca4cb152c5dd98e1bd917c52fd46fc80a',\n    b'3c7fb05b0ab4da8b292e895f574f8213cadfe81b84ded7423eab61c5f884c8ae',\n    b'b21fc7be1e69182827538683a48ac9d95684faf6c1c6deabb6e513d8c76afcc9',\n    b'a5021734dbbf1d090b15509ba00f2c04a3d5afc19939b4594ca0850d4190b923',\n    b'07dfe0aa94c1b948dc935be1f8179f3050353b46f3a3134e77c70e66208be72d',\n    b'c331b2fb82cd91120b0703ee312042a854a51a8d945aa9e70fb14d68b0366fe1',\n    b'3aa59ec4d8f1e7ce2775854b5e82433535b6e3503f9a8e7c4e60aac066d44718',\n    b'ccc8b4ca73b266b4a0c89a9d33c4ec7532b434c9294c26832355e5e2bee2e005',\n    b'280c074d8982e56d70c404072252c309694a6e5c05457a6abbe8fc225c2dfd52',\n    b'546cee26da713a3a64b2066d5e3a52b7c1d927396d1ba8a3d9f6e3e973398856',\n    b'7fbc4615d5e819eee22db440c5bcc4ff25bb046841c41a192003a6d9abfbafbf',\n    b'5b63f13011cab965feea3a41fac2d7a877aa710ab20e2a9a1708474e3c05c050',\n    b'394b36f528947557d317fd40a4adde5514c8745a5f64185421fa2c0c4a158938',\n    b'8f101c8f5290ae6c0dd76d210b7effacd7f12db18f3befab711f533bde084c76',\n    b'6637a656f897a66080fbe60027d32c3f4ebc0e3b5f96123a33f932a091b039c2',\n    b'2815aa6667c042a3a4565fb789890cd33e380d047ed712759d097d479df71051',\n    b'120e761c6382b07a9548650a20b3b9dd74b906093260fa6f92f790ba71f79e8d',\n    b'823c8a613ea539f730a968518993195174bf973ed75c734b6898022867165d7b'\n]\nexpected_hardened_privkeys = [\n    b'abdba45b0459e7804beb68edb899e58a5c2636bf67d096711904001406afbd4c',\n    b'c9e804d4b8fdd99ef6ab2b0ca627a57f4283c28e11e9152ad9d3f863404d940e',\n    b'4cf87d68ae99711261f8cb8e1bde83b8703ff5d689ef70ce23106d1e6e8ed4bd',\n    b'dbf8d578c77f9bf62bb2ad40975e253af1e1d44d53abf84a22d2be29b9488f7f',\n    b'633bb840505521ffe39cb89a04fb8bff3298d6b64a5d8f170aca1e456d6f89b9',\n    b'92e80a38791bd8ba2105b9867fd58ac2cc4fb9853e18141b7fee1884bc5aae69',\n    b'd3663339af1386d05dd90ee20f627661ae87ddb1db0c2dc73fd8a4485930d0e7',\n    b'09a448303452d241b8a25670b36cc758975b97e88f62b6f25cd9084535e3c13a',\n    b'ee22eb77df05ff53e9c2ba797c1f2ebf97ec4cf5a99528adec94972674aeabed',\n    b'935facccb6120659c5b7c606a457c797e5a10ce4a728346e1a3a963251169651',\n    b'8ac9b4a48da1def375640ca03bc6711040dfd4eea7106d42bb4c2de83d7f595e',\n    b'51ecd3f7565c2b86d5782dbde2175ab26a7b896022564063fafe153588610be9',\n    b'04918252f6b6f51cd75957289b56a324b45cc085df80839137d740f9ada6c062',\n    b'2efbd0c839af971e3769c26938d776990ebf097989df4861535a7547a2701483',\n    b'85c6e31e6b27bd188291a910f4a7faba7fceb3e09df72884b10907ecc1491cd0',\n    b'05e245131885bebda993a31bb14ac98b794062a50af639ad22010aed1e533a54',\n    b'ddca42cf7db93f3a3f0723d5fee4c21bf60b7afac35d5c30eb34bd91b35cc609',\n    b'324a5c16030e0c3947e4dcd2b5057fd3a4d5bed96b23e3b476b2af0ab76369c9',\n    b'da63c41cdb398cdcd93e832f3e198528afbb4065821b026c143cec910d8362f0'\n]\n"
  },
  {
    "path": "tests/unit/wallet/server/__init__.py",
    "content": ""
  },
  {
    "path": "tests/unit/wallet/server/test_migration.py",
    "content": "# import unittest\n# from shutil import rmtree\n# from tempfile import mkdtemp\n#\n# from lbry.wallet.server.history import History\n# from lbry.wallet.server.storage import LevelDB\n#\n#\n# # dumped from a real history database. Aside from the state, all records are <hashX><flush_count>: <value>\n# STATE_RECORD = (b'state\\x00\\x00', b\"{'flush_count': 21497, 'comp_flush_count': -1, 'comp_cursor': -1, 'db_version': 0}\")\n# UNMIGRATED_RECORDS = {\n#     '00538b2cbe4a5f1be2dc320241': 'f5ed500142ee5001',\n#     '00538b48def1904014880501f2': 'b9a52a01baa52a01',\n#     '00538cdcf989b74de32c5100ca': 'c973870078748700',\n#     '00538d42d5df44603474284ae1': 'f5d9d802',\n#     '00538d42d5df44603474284ae2': '75dad802',\n#     '00538ebc879dac6ddbee9e0029': '3ca42f0042a42f00',\n#     '00538ed1d391327208748200bc': '804e7d00af4e7d00',\n#     '00538f3de41d9e33affa0300c2': '7de8810086e88100',\n#     '00539007f87792d98422c505a5': '8c5a7202445b7202',\n#     '0053902cf52ee9682d633b0575': 'eb0f64026c106402',\n#     '005390e05674571551632205a2': 'a13d7102e13d7102',\n#     '0053914ef25a9ceed927330584': '78096902960b6902',\n#     '005391768113f69548f37a01b1': 'a5b90b0114ba0b01',\n#     '005391a289812669e5b44c02c2': '33da8a016cdc8a01',\n# }\n#\n#\n# class TestHistoryDBMigration(unittest.TestCase):\n#     def test_migrate_flush_count_from_16_to_32_bits(self):\n#         self.history = History()\n#         tmpdir = mkdtemp()\n#         self.addCleanup(lambda: rmtree(tmpdir))\n#         LevelDB.import_module()\n#         db = LevelDB(tmpdir, 'hist', True)\n#         with db.write_batch() as batch:\n#             for key, value in UNMIGRATED_RECORDS.items():\n#                 batch.put(bytes.fromhex(key), bytes.fromhex(value))\n#             batch.put(*STATE_RECORD)\n#         self.history.db = db\n#         self.history.read_state()\n#         self.assertEqual(21497, self.history.flush_count)\n#         self.assertEqual(0, self.history.db_version)\n#         self.assertTrue(self.history.needs_migration)\n#         self.history.migrate()\n#         self.assertFalse(self.history.needs_migration)\n#         self.assertEqual(1, self.history.db_version)\n#         for idx, (key, value) in enumerate(sorted(db.iterator())):\n#             if key == b'state\\x00\\x00':\n#                 continue\n#             key, counter = key[:-4], key[-4:]\n#             expected_value = UNMIGRATED_RECORDS[key.hex() + counter.hex()[-4:]]\n#             self.assertEqual(value.hex(), expected_value)\n#\n#\n# if __name__ == '__main__':\n#     unittest.main()\n"
  },
  {
    "path": "tests/unit/wallet/test_account.py",
    "content": "import asyncio\nfrom binascii import hexlify\nfrom lbry.testcase import AsyncioTestCase\nfrom lbry.wallet import (\n    Wallet, Ledger, Database, Headers,\n    Account, SingleKey, HierarchicalDeterministic,\n    DeterministicChannelKeyManager\n)\n\n\nclass TestAccount(AsyncioTestCase):\n\n    async def asyncSetUp(self):\n        self.ledger = Ledger({\n            'db': Database(':memory:'),\n            'headers': Headers(':memory:')\n        })\n        await self.ledger.db.open()\n\n    async def asyncTearDown(self):\n        await self.ledger.db.close()\n\n    async def test_generate_account(self):\n        account = Account.generate(self.ledger, Wallet(), 'lbryum')\n        self.assertEqual(account.ledger, self.ledger)\n        self.assertIsNotNone(account.seed)\n        self.assertEqual(account.public_key.ledger, self.ledger)\n        self.assertEqual(account.private_key.public_key, account.public_key)\n\n        self.assertEqual(account.public_key.ledger, self.ledger)\n        self.assertEqual(account.private_key.public_key, account.public_key)\n\n        addresses = await account.receiving.get_addresses()\n        self.assertEqual(len(addresses), 0)\n        addresses = await account.change.get_addresses()\n        self.assertEqual(len(addresses), 0)\n\n        await account.ensure_address_gap()\n\n        addresses = await account.receiving.get_addresses()\n        self.assertEqual(len(addresses), 20)\n        addresses = await account.change.get_addresses()\n        self.assertEqual(len(addresses), 6)\n\n    async def test_unused_address_on_account_creation_does_not_cause_a_race(self):\n        account = Account.generate(self.ledger, Wallet(), 'lbryum')\n        await account.ledger.db.db.executescript(\"update pubkey_address set used_times=10\")\n        await account.receiving.address_generator_lock.acquire()\n        delayed1 = asyncio.ensure_future(account.receiving.ensure_address_gap())\n        delayed = asyncio.ensure_future(account.receiving.get_or_create_usable_address())\n        await asyncio.sleep(0)\n        # wallet being created and queried at the same time\n        account.receiving.address_generator_lock.release()\n        await delayed1\n        await delayed\n\n    async def test_generate_keys_over_batch_threshold_saves_it_properly(self):\n        account = Account.generate(self.ledger, Wallet(), 'lbryum')\n        async with account.receiving.address_generator_lock:\n            await account.receiving._generate_keys(0, 200)\n        records = await account.receiving.get_address_records()\n        self.assertEqual(len(records), 201)\n\n    async def test_ensure_address_gap(self):\n        account = Account.generate(self.ledger, Wallet(), 'lbryum')\n\n        self.assertIsInstance(account.receiving, HierarchicalDeterministic)\n\n        async with account.receiving.address_generator_lock:\n            await account.receiving._generate_keys(4, 7)\n            await account.receiving._generate_keys(0, 3)\n            await account.receiving._generate_keys(8, 11)\n        records = await account.receiving.get_address_records()\n        self.assertListEqual(\n            [r['pubkey'].n for r in records],\n            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]\n        )\n\n        # we have 12, but default gap is 20\n        new_keys = await account.receiving.ensure_address_gap()\n        self.assertEqual(len(new_keys), 8)\n        records = await account.receiving.get_address_records()\n        self.assertListEqual(\n            [r['pubkey'].n for r in records],\n            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]\n        )\n\n        # case #1: no new addresses needed\n        empty = await account.receiving.ensure_address_gap()\n        self.assertEqual(len(empty), 0)\n\n        # case #2: only one new addressed needed\n        records = await account.receiving.get_address_records()\n        await self.ledger.db.set_address_history(records[0]['address'], 'a:1:')\n        new_keys = await account.receiving.ensure_address_gap()\n        self.assertEqual(len(new_keys), 1)\n\n        # case #3: 20 addresses needed\n        await self.ledger.db.set_address_history(new_keys[0], 'a:1:')\n        new_keys = await account.receiving.ensure_address_gap()\n        self.assertEqual(len(new_keys), 20)\n\n    async def test_get_or_create_usable_address(self):\n        account = Account.generate(self.ledger, Wallet(), 'lbryum')\n\n        keys = await account.receiving.get_addresses()\n        self.assertEqual(len(keys), 0)\n\n        address = await account.receiving.get_or_create_usable_address()\n        self.assertIsNotNone(address)\n\n        keys = await account.receiving.get_addresses()\n        self.assertEqual(len(keys), 20)\n\n    async def test_generate_account_from_seed(self):\n        account = Account.from_dict(\n            self.ledger, Wallet(), {\n                \"seed\":\n                    \"carbon smart garage balance margin twelve chest sword toas\"\n                    \"t envelope bottom stomach absent\"\n            }\n        )\n        self.assertEqual(\n            account.private_key.extended_key_string(),\n            'xprv9s21ZrQH143K42ovpZygnjfHdAqSd9jo7zceDfPRogM7bkkoNVv7DRNLEoB8'\n            'HoirMgH969NrgL8jNzLEegqFzPRWM37GXd4uE8uuRkx4LAe'\n        )\n        self.assertEqual(\n            account.public_key.extended_key_string(),\n            'xpub661MyMwAqRbcGWtPvbWh9sc2BCfw2cTeVDYF23o3N1t6UZ5wv3EMmDgp66FxH'\n            'uDtWdft3B5eL5xQtyzAtkdmhhC95gjRjLzSTdkho95asu9'\n        )\n        address = await account.receiving.ensure_address_gap()\n        self.assertEqual(address[0], 'bCqJrLHdoiRqEZ1whFZ3WHNb33bP34SuGx')\n\n        private_key = await self.ledger.get_private_key_for_address(\n            account.wallet, 'bCqJrLHdoiRqEZ1whFZ3WHNb33bP34SuGx'\n        )\n        self.assertEqual(\n            private_key.extended_key_string(),\n            'xprv9vwXVierUTT4hmoe3dtTeBfbNv1ph2mm8RWXARU6HsZjBaAoFaS2FRQu4fptR'\n            'AyJWhJW42dmsEaC1nKnVKKTMhq3TVEHsNj1ca3ciZMKktT'\n        )\n        private_key = await self.ledger.get_private_key_for_address(\n            account.wallet, 'BcQjRlhDOIrQez1WHfz3whnB33Bp34sUgX'\n        )\n        self.assertIsNone(private_key)\n\n    async def test_load_and_save_account(self):\n        account_data = {\n            'name': 'Main Account',\n            'modified_on': 123,\n            'seed':\n                \"carbon smart garage balance margin twelve chest sword toast envelope bottom stomac\"\n                \"h absent\",\n            'encrypted': False,\n            'private_key':\n                'xprv9s21ZrQH143K42ovpZygnjfHdAqSd9jo7zceDfPRogM7bkkoNVv7DRNLEoB8'\n                'HoirMgH969NrgL8jNzLEegqFzPRWM37GXd4uE8uuRkx4LAe',\n            'public_key':\n                'xpub661MyMwAqRbcGWtPvbWh9sc2BCfw2cTeVDYF23o3N1t6UZ5wv3EMmDgp66FxH'\n                'uDtWdft3B5eL5xQtyzAtkdmhhC95gjRjLzSTdkho95asu9',\n            'certificates': {},\n            'address_generator': {\n                'name': 'deterministic-chain',\n                'receiving': {'gap': 17, 'maximum_uses_per_address': 2},\n                'change': {'gap': 10, 'maximum_uses_per_address': 2}\n            }\n        }\n\n        account = Account.from_dict(self.ledger, Wallet(), account_data)\n\n        await account.ensure_address_gap()\n\n        addresses = await account.receiving.get_addresses()\n        self.assertEqual(len(addresses), 17)\n        addresses = await account.change.get_addresses()\n        self.assertEqual(len(addresses), 10)\n\n        account_data['ledger'] = 'lbc_mainnet'\n        self.assertDictEqual(account_data, account.to_dict())\n\n    async def test_save_max_gap(self):\n        account = Account.generate(\n            self.ledger, Wallet(), 'lbryum', {\n                    'name': 'deterministic-chain',\n                    'receiving': {'gap': 3, 'maximum_uses_per_address': 2},\n                    'change': {'gap': 4, 'maximum_uses_per_address': 2}\n                }\n        )\n        self.assertEqual(account.receiving.gap, 3)\n        self.assertEqual(account.change.gap, 4)\n        await account.save_max_gap()\n        self.assertEqual(account.receiving.gap, 20)\n        self.assertEqual(account.change.gap, 6)\n        # doesn't fail for single-address account\n        account2 = Account.generate(self.ledger, Wallet(), 'lbryum', {'name': 'single-address'})\n        await account2.save_max_gap()\n\n    def test_merge_diff(self):\n        account_data = {\n            'name': 'My Account',\n            'modified_on': 123,\n            'seed':\n                \"carbon smart garage balance margin twelve chest sword toast envelope bottom stomac\"\n                \"h absent\",\n            'encrypted': False,\n            'private_key':\n                'xprv9s21ZrQH143K3TsAz5efNV8K93g3Ms3FXcjaWB9fVUsMwAoE3ZT4vYymkp'\n                '5BxKKfnpz8J6sHDFriX1SnpvjNkzcks8XBnxjGLS83BTyfpna',\n            'public_key':\n                'xpub661MyMwAqRbcFwwe67Bfjd53h5WXmKm6tqfBJZZH3pQLoy8Nb6mKUMJFc7'\n                'UbpVNzmwFPN2evn3YHnig1pkKVYcvCV8owTd2yAcEkJfCX53g',\n            'address_generator': {\n                'name': 'deterministic-chain',\n                'receiving': {'gap': 5, 'maximum_uses_per_address': 2},\n                'change': {'gap': 5, 'maximum_uses_per_address': 2}\n            }\n        }\n        account = Account.from_dict(self.ledger, Wallet(), account_data)\n\n        self.assertEqual(account.name, 'My Account')\n        self.assertEqual(account.modified_on, 123)\n        self.assertEqual(account.change.gap, 5)\n        self.assertEqual(account.change.maximum_uses_per_address, 2)\n        self.assertEqual(account.receiving.gap, 5)\n        self.assertEqual(account.receiving.maximum_uses_per_address, 2)\n\n        account_data['name'] = 'Changed Name'\n        account_data['address_generator']['change']['gap'] = 6\n        account_data['address_generator']['change']['maximum_uses_per_address'] = 7\n        account_data['address_generator']['receiving']['gap'] = 8\n        account_data['address_generator']['receiving']['maximum_uses_per_address'] = 9\n\n        account.merge(account_data)\n        # no change because modified_on is not newer\n        self.assertEqual(account.name, 'My Account')\n\n        account_data['modified_on'] = 200.00\n\n        account.merge(account_data)\n        self.assertEqual(account.name, 'Changed Name')\n        self.assertEqual(account.change.gap, 6)\n        self.assertEqual(account.change.maximum_uses_per_address, 7)\n        self.assertEqual(account.receiving.gap, 8)\n        self.assertEqual(account.receiving.maximum_uses_per_address, 9)\n\n\nclass TestSingleKeyAccount(AsyncioTestCase):\n\n    async def asyncSetUp(self):\n        self.ledger = Ledger({\n            'db': Database(':memory:'),\n            'headers': Headers(':memory:')\n        })\n        await self.ledger.db.open()\n        self.account = Account.generate(self.ledger, Wallet(), \"torba\", {'name': 'single-address'})\n\n    async def asyncTearDown(self):\n        await self.ledger.db.close()\n\n    async def test_generate_account(self):\n        account = self.account\n\n        self.assertEqual(account.ledger, self.ledger)\n        self.assertIsNotNone(account.seed)\n        self.assertEqual(account.public_key.ledger, self.ledger)\n        self.assertEqual(account.private_key.public_key, account.public_key)\n\n        addresses = await account.receiving.get_addresses()\n        self.assertEqual(len(addresses), 0)\n        addresses = await account.change.get_addresses()\n        self.assertEqual(len(addresses), 0)\n\n        await account.ensure_address_gap()\n\n        addresses = await account.receiving.get_addresses()\n        self.assertEqual(len(addresses), 1)\n        self.assertEqual(addresses[0], account.public_key.address)\n        addresses = await account.change.get_addresses()\n        self.assertEqual(len(addresses), 1)\n        self.assertEqual(addresses[0], account.public_key.address)\n\n        addresses = await account.get_addresses()\n        self.assertEqual(len(addresses), 1)\n        self.assertEqual(addresses[0], account.public_key.address)\n\n    async def test_ensure_address_gap(self):\n        account = self.account\n\n        self.assertIsInstance(account.receiving, SingleKey)\n        addresses = await account.receiving.get_addresses()\n        self.assertListEqual(addresses, [])\n\n        # we have 12, but default gap is 20\n        new_keys = await account.receiving.ensure_address_gap()\n        self.assertEqual(len(new_keys), 1)\n        self.assertEqual(new_keys[0], account.public_key.address)\n        records = await account.receiving.get_address_records()\n        pubkey = records[0].pop('pubkey')\n        self.assertListEqual(records, [{\n            'chain': 0,\n            'account': account.public_key.address,\n            'address': account.public_key.address,\n            'history': None,\n            'used_times': 0\n        }])\n        self.assertEqual(\n            pubkey.extended_key_string(),\n            account.public_key.extended_key_string()\n        )\n\n        # case #1: no new addresses needed\n        empty = await account.receiving.ensure_address_gap()\n        self.assertEqual(len(empty), 0)\n\n        # case #2: after use, still no new address needed\n        records = await account.receiving.get_address_records()\n        await self.ledger.db.set_address_history(records[0]['address'], 'a:1:')\n        empty = await account.receiving.ensure_address_gap()\n        self.assertEqual(len(empty), 0)\n\n    async def test_get_or_create_usable_address(self):\n        account = self.account\n\n        addresses = await account.receiving.get_addresses()\n        self.assertEqual(len(addresses), 0)\n\n        address1 = await account.receiving.get_or_create_usable_address()\n        self.assertIsNotNone(address1)\n\n        await self.ledger.db.set_address_history(address1, 'a:1:b:2:c:3:')\n        records = await account.receiving.get_address_records()\n        self.assertEqual(records[0]['used_times'], 3)\n\n        address2 = await account.receiving.get_or_create_usable_address()\n        self.assertEqual(address1, address2)\n\n        keys = await account.receiving.get_addresses()\n        self.assertEqual(len(keys), 1)\n\n    async def test_generate_account_from_seed(self):\n        account = Account.from_dict(\n            self.ledger, Wallet(), {\n                \"seed\":\n                    \"carbon smart garage balance margin twelve chest sword toas\"\n                    \"t envelope bottom stomach absent\",\n                'address_generator': {'name': 'single-address'}\n            }\n        )\n        self.assertEqual(\n            account.private_key.extended_key_string(),\n            'xprv9s21ZrQH143K42ovpZygnjfHdAqSd9jo7zceDfPRogM7bkkoNVv7'\n            'DRNLEoB8HoirMgH969NrgL8jNzLEegqFzPRWM37GXd4uE8uuRkx4LAe',\n        )\n        self.assertEqual(\n            account.public_key.extended_key_string(),\n            'xpub661MyMwAqRbcGWtPvbWh9sc2BCfw2cTeVDYF23o3N1t6UZ5wv3EM'\n            'mDgp66FxHuDtWdft3B5eL5xQtyzAtkdmhhC95gjRjLzSTdkho95asu9',\n        )\n        address = await account.receiving.ensure_address_gap()\n        self.assertEqual(address[0], account.public_key.address)\n\n        private_key = await self.ledger.get_private_key_for_address(\n            account.wallet, address[0]\n        )\n        self.assertEqual(\n            private_key.extended_key_string(),\n            'xprv9s21ZrQH143K42ovpZygnjfHdAqSd9jo7zceDfPRogM7bkkoNVv7'\n            'DRNLEoB8HoirMgH969NrgL8jNzLEegqFzPRWM37GXd4uE8uuRkx4LAe',\n        )\n\n        invalid_key = await self.ledger.get_private_key_for_address(\n            account.wallet, 'BcQjRlhDOIrQez1WHfz3whnB33Bp34sUgX'\n        )\n        self.assertIsNone(invalid_key)\n\n        self.assertEqual(\n            hexlify(private_key.wif()),\n            b'1cef6c80310b1bcbcfa3176ea809ac840f48cda634c475d402e6bd68d5bb3827d601'\n        )\n\n    async def test_load_and_save_account(self):\n        account_data = {\n            'name': 'My Account',\n            'modified_on': 123,\n            'seed':\n                \"carbon smart garage balance margin twelve chest sword toast envelope bottom stomac\"\n                \"h absent\",\n            'encrypted': False,\n            'private_key': 'xprv9s21ZrQH143K42ovpZygnjfHdAqSd9jo7zceDfPRogM7bkkoNVv7'\n                           'DRNLEoB8HoirMgH969NrgL8jNzLEegqFzPRWM37GXd4uE8uuRkx4LAe',\n            'public_key': 'xpub661MyMwAqRbcGWtPvbWh9sc2BCfw2cTeVDYF23o3N1t6UZ5wv3EM'\n                          'mDgp66FxHuDtWdft3B5eL5xQtyzAtkdmhhC95gjRjLzSTdkho95asu9',\n            'address_generator': {'name': 'single-address'},\n            'certificates': {}\n        }\n\n        account = Account.from_dict(self.ledger, Wallet(), account_data)\n\n        await account.ensure_address_gap()\n\n        addresses = await account.receiving.get_addresses()\n        self.assertEqual(len(addresses), 1)\n        addresses = await account.change.get_addresses()\n        self.assertEqual(len(addresses), 1)\n\n        self.maxDiff = None\n        account_data['ledger'] = 'lbc_mainnet'\n        self.assertDictEqual(account_data, account.to_dict())\n\n\nclass AccountEncryptionTests(AsyncioTestCase):\n    password = \"password\"\n    init_vector = b'0000000000000000'\n    unencrypted_account = {\n        'name': 'My Account',\n        'seed':\n            \"carbon smart garage balance margin twelve chest sword toast envelope bottom stomac\"\n            \"h absent\",\n        'encrypted': False,\n        'private_key':\n            'xprv9s21ZrQH143K42ovpZygnjfHdAqSd9jo7zceDfPRogM7bkkoNVv7DRNLEo'\n            'B8HoirMgH969NrgL8jNzLEegqFzPRWM37GXd4uE8uuRkx4LAe',\n        'public_key':\n            'xpub661MyMwAqRbcFwwe67Bfjd53h5WXmKm6tqfBJZZH3pQLoy8Nb6mKUMJFc7'\n            'UbpVNzmwFPN2evn3YHnig1pkKVYcvCV8owTd2yAcEkJfCX53g',\n        'address_generator': {'name': 'single-address'}\n    }\n    encrypted_account = {\n        'name': 'My Account',\n        'seed':\n            \"MDAwMDAwMDAwMDAwMDAwMJ4e4W4pE6nQtPiD6MujNIQ7aFPhUBl63GwPziAgGN\"\n            \"MBTMoaSjZfyyvw7ELMCqAYTWJ61aV7K4lmd2hR11g9dpdnnpCb9f9j3zLZHRv7+\"\n            \"bIkZ//trah9AIkmrc/ZvNkC0Q==\",\n        'encrypted': True,\n        'private_key':\n            'MDAwMDAwMDAwMDAwMDAwMLkWikOLScA/ZxlFSGU7dl8pqVjgdpu1S3MWQF3IJ5H'\n            'OXPAQcgnhHldVq98uP7Q8JqSWOv1p4gpxGSYnA4w5Gbuh0aUD4hmV70m7nVTj7T'\n            '15+Pu30DCspndru59pee/S+mShoK68q7t7r32leaVIfzw=',\n        'public_key':\n            'xpub661MyMwAqRbcFwwe67Bfjd53h5WXmKm6tqfBJZZH3pQLoy8Nb6mKUMJFc7'\n            'UbpVNzmwFPN2evn3YHnig1pkKVYcvCV8owTd2yAcEkJfCX53g',\n        'address_generator': {'name': 'single-address'}\n    }\n\n    async def asyncSetUp(self):\n        self.ledger = Ledger({\n            'db': Database(':memory:'),\n            'headers': Headers(':memory:')\n        })\n\n    def test_encrypt_wallet(self):\n        account = Account.from_dict(self.ledger, Wallet(), self.unencrypted_account)\n        account.init_vectors = {\n            'seed': self.init_vector,\n            'private_key': self.init_vector\n        }\n\n        self.assertFalse(account.encrypted)\n        self.assertIsNotNone(account.private_key)\n        account.encrypt(self.password)\n        self.assertTrue(account.encrypted)\n        self.assertEqual(account.seed, self.encrypted_account['seed'])\n        self.assertEqual(account.private_key_string, self.encrypted_account['private_key'])\n        self.assertIsNone(account.private_key)\n\n        self.assertEqual(account.to_dict()['seed'], self.encrypted_account['seed'])\n        self.assertEqual(account.to_dict()['private_key'], self.encrypted_account['private_key'])\n\n        account.decrypt(self.password)\n        self.assertEqual(account.init_vectors['private_key'], self.init_vector)\n        self.assertEqual(account.init_vectors['seed'], self.init_vector)\n\n        self.assertEqual(account.seed, self.unencrypted_account['seed'])\n        self.assertEqual(account.private_key.extended_key_string(), self.unencrypted_account['private_key'])\n\n        self.assertEqual(account.to_dict(encrypt_password=self.password)['seed'], self.encrypted_account['seed'])\n        self.assertEqual(account.to_dict(encrypt_password=self.password)['private_key'], self.encrypted_account['private_key'])\n\n        self.assertFalse(account.encrypted)\n\n    def test_decrypt_wallet(self):\n        account = Account.from_dict(self.ledger, Wallet(), self.encrypted_account)\n\n        self.assertTrue(account.encrypted)\n        account.decrypt(self.password)\n        self.assertEqual(account.init_vectors['private_key'], self.init_vector)\n        self.assertEqual(account.init_vectors['seed'], self.init_vector)\n\n        self.assertFalse(account.encrypted)\n\n        self.assertEqual(account.seed, self.unencrypted_account['seed'])\n        self.assertEqual(account.private_key.extended_key_string(), self.unencrypted_account['private_key'])\n\n        self.assertEqual(account.to_dict(encrypt_password=self.password)['seed'], self.encrypted_account['seed'])\n        self.assertEqual(account.to_dict(encrypt_password=self.password)['private_key'], self.encrypted_account['private_key'])\n        self.assertEqual(account.to_dict()['seed'], self.unencrypted_account['seed'])\n        self.assertEqual(account.to_dict()['private_key'], self.unencrypted_account['private_key'])\n\n    def test_encrypt_decrypt_read_only_account(self):\n        account_data = self.unencrypted_account.copy()\n        del account_data['seed']\n        del account_data['private_key']\n        account = Account.from_dict(self.ledger, Wallet(), account_data)\n        encrypted = account.to_dict('password')\n        self.assertFalse(encrypted['seed'])\n        self.assertFalse(encrypted['private_key'])\n        account.encrypt('password')\n        account.decrypt('password')\n"
  },
  {
    "path": "tests/unit/wallet/test_bcd_data_stream.py",
    "content": "import unittest\n\nfrom lbry.wallet.bcd_data_stream import BCDataStream\n\n\nclass TestBCDataStream(unittest.TestCase):\n\n    def test_write_read(self):\n        s = BCDataStream()\n        s.write_string(b'a'*252)\n        s.write_string(b'b'*254)\n        s.write_string(b'c'*(0xFFFF + 1))\n        # s.write_string(b'd'*(0xFFFFFFFF + 1))\n        s.write_boolean(True)\n        s.write_boolean(False)\n        s.reset()\n\n        self.assertEqual(s.read_string(), b'a'*252)\n        self.assertEqual(s.read_string(), b'b'*254)\n        self.assertEqual(s.read_string(), b'c'*(0xFFFF + 1))\n        # self.assertEqual(s.read_string(), b'd'*(0xFFFFFFFF + 1))\n        self.assertTrue(s.read_boolean())\n        self.assertFalse(s.read_boolean())\n"
  },
  {
    "path": "tests/unit/wallet/test_bip32.py",
    "content": "from binascii import unhexlify, hexlify\n\nfrom lbry.testcase import AsyncioTestCase\nfrom lbry.wallet.bip32 import PublicKey, PrivateKey, from_extended_key_string\nfrom lbry.wallet import Ledger, Database, Headers\n\nfrom tests.unit.wallet.key_fixtures import expected_ids, expected_privkeys, expected_hardened_privkeys\n\n\nclass BIP32Tests(AsyncioTestCase):\n\n    def test_pubkey_validation(self):\n        with self.assertRaisesRegex(TypeError, 'chain code must be raw bytes'):\n            PublicKey(None, None, 1, None, None, None)\n        with self.assertRaisesRegex(ValueError, 'invalid chain code'):\n            PublicKey(None, None, b'abcd', None, None, None)\n        with self.assertRaisesRegex(ValueError, 'invalid child number'):\n            PublicKey(None, None, b'abcd'*8, -1, None, None)\n        with self.assertRaisesRegex(ValueError, 'invalid depth'):\n            PublicKey(None, None, b'abcd'*8, 0, 256, None)\n        with self.assertRaisesRegex(TypeError, 'pubkey must be raw bytes'):\n            PublicKey(None, None, b'abcd'*8, 0, 255, None)\n        with self.assertRaisesRegex(ValueError, 'pubkey must be 33 bytes'):\n            PublicKey(None, b'abcd', b'abcd'*8, 0, 255, None)\n        with self.assertRaisesRegex(ValueError, 'invalid pubkey prefix byte'):\n            PublicKey(\n                None,\n                unhexlify('33d1a3dc8155673bc1e2214fa493ccc82d57961b66054af9b6b653ac28eeef3ffe'),\n                b'abcd'*8, 0, 255, None\n            )\n        pubkey = PublicKey(  # success\n            None,\n            unhexlify('03d1a3dc8155673bc1e2214fa493ccc82d57961b66054af9b6b653ac28eeef3ffe'),\n            b'abcd'*8, 0, 1, None\n        )\n        with self.assertRaisesRegex(ValueError, 'invalid BIP32 public key child number'):\n            pubkey.child(-1)\n        for i in range(20):\n            new_key = pubkey.child(i)\n            self.assertIsInstance(new_key, PublicKey)\n            self.assertEqual(hexlify(new_key.identifier()), expected_ids[i])\n\n    async def test_private_key_validation(self):\n        with self.assertRaisesRegex(TypeError, 'private key must be raw bytes'):\n            PrivateKey(None, None, b'abcd'*8, 0, 255)\n        with self.assertRaisesRegex(ValueError, 'private key must be 32 bytes'):\n            PrivateKey(None, b'abcd', b'abcd'*8, 0, 255)\n        private_key = PrivateKey(\n            Ledger({\n                'db': Database(':memory:'),\n                'headers': Headers(':memory:'),\n            }),\n            unhexlify('2423f3dc6087d9683f73a684935abc0ccd8bc26370588f56653128c6a6f0bf7c'),\n            b'abcd'*8, 0, 1\n        )\n        ec_point = private_key.ec_point()\n        self.assertEqual(\n            ec_point[0], 30487144161998778625547553412379759661411261804838752332906558028921886299019\n        )\n        self.assertEqual(\n            ec_point[1], 86198965946979720220333266272536217633917099472454294641561154971209433250106\n        )\n        self.assertEqual('bUDcmraBp2zCV3QWmVVeQaEgepbs1b2gC9', private_key.address)\n        with self.assertRaisesRegex(ValueError, 'invalid BIP32 private key child number'):\n            private_key.child(-1)\n        self.assertIsInstance(private_key.child(PrivateKey.HARDENED), PrivateKey)\n\n    async def test_private_key_derivation(self):\n        private_key = PrivateKey(\n            Ledger({\n                'db': Database(':memory:'),\n                'headers': Headers(':memory:'),\n            }),\n            unhexlify('2423f3dc6087d9683f73a684935abc0ccd8bc26370588f56653128c6a6f0bf7c'),\n            b'abcd'*8, 0, 1\n        )\n        for i in range(20):\n            new_privkey = private_key.child(i)\n            self.assertIsInstance(new_privkey, PrivateKey)\n            self.assertEqual(hexlify(new_privkey.private_key_bytes), expected_privkeys[i])\n        for i in range(PrivateKey.HARDENED + 1, private_key.HARDENED + 20):\n            new_privkey = private_key.child(i)\n            self.assertIsInstance(new_privkey, PrivateKey)\n            self.assertEqual(hexlify(new_privkey.private_key_bytes), expected_hardened_privkeys[i - 1 - PrivateKey.HARDENED])\n\n    async def test_from_extended_keys(self):\n        ledger = Ledger({\n            'db': Database(':memory:'),\n            'headers': Headers(':memory:'),\n        })\n        self.assertIsInstance(\n            from_extended_key_string(\n                ledger,\n                'xprv9s21ZrQH143K2dyhK7SevfRG72bYDRNv25yKPWWm6dqApNxm1Zb1m5gGcBWYfbsPjTr2v5joit8Af2Zp5P'\n                '6yz3jMbycrLrRMpeAJxR8qDg8',\n            ), PrivateKey\n        )\n        self.assertIsInstance(\n            from_extended_key_string(\n                ledger,\n                'xpub661MyMwAqRbcF84AR8yfHoMzf4S2ct6mPJtvBtvNeyN9hBHuZ6uGJszkTSn5fQUCdz3XU17eBzFeAUwV6f'\n                'iW44g14WF52fYC5J483wqQ5ZP',\n            ), PublicKey\n        )\n"
  },
  {
    "path": "tests/unit/wallet/test_claim_proofs.py",
    "content": "import unittest\nfrom binascii import hexlify, unhexlify\n\nfrom lbry.wallet.claim_proofs import get_hash_for_outpoint, verify_proof\nfrom lbry.crypto.hash import double_sha256\n\n\nclass ClaimProofsTestCase(unittest.TestCase):\n    def test_verify_proof(self):\n        claim1_name = 97  # 'a'\n        claim1_txid = 'bd9fa7ffd57d810d4ce14de76beea29d847b8ac34e8e536802534ecb1ca43b68'\n        claim1_outpoint = 0\n        claim1_height = 10\n        claim1_node_hash = get_hash_for_outpoint(\n            unhexlify(claim1_txid)[::-1], claim1_outpoint, claim1_height)\n\n        claim2_name = 98  # 'b'\n        claim2_txid = 'ad9fa7ffd57d810d4ce14de76beea29d847b8ac34e8e536802534ecb1ca43b68'\n        claim2_outpoint = 1\n        claim2_height = 5\n        claim2_node_hash = get_hash_for_outpoint(\n            unhexlify(claim2_txid)[::-1], claim2_outpoint, claim2_height)\n        to_hash1 = claim1_node_hash\n        hash1 = double_sha256(to_hash1)\n        to_hash2 = bytes((claim1_name,)) + hash1 + bytes((claim2_name,)) + claim2_node_hash\n\n        root_hash = double_sha256(to_hash2)\n\n        proof = {\n            'last takeover height': claim1_height, 'txhash': claim1_txid, 'nOut': claim1_outpoint,\n            'nodes': [\n                {'children': [\n                    {'character': 97},\n                    {\n                        'character': 98,\n                        'nodeHash': hexlify(claim2_node_hash[::-1])\n                    }\n                ]},\n                {'children': []},\n            ]\n        }\n        out = verify_proof(proof, hexlify(root_hash[::-1]), 'a')\n        self.assertTrue(out)\n"
  },
  {
    "path": "tests/unit/wallet/test_coinselection.py",
    "content": "from types import GeneratorType\n\nfrom lbry.testcase import AsyncioTestCase\n\nfrom lbry.wallet import Ledger, Database, Headers\nfrom lbry.wallet.coinselection import CoinSelector, MAXIMUM_TRIES\nfrom lbry.constants import CENT\n\nfrom tests.unit.wallet.test_transaction import get_output as utxo\n\n\nNULL_HASH = b'\\x00'*32\n\n\ndef search(*args, **kwargs):\n    selection = CoinSelector(*args[1:], **kwargs).select(args[0], 'branch_and_bound')\n    return [o.txo.amount for o in selection] if selection else selection\n\n\nclass BaseSelectionTestCase(AsyncioTestCase):\n\n    async def asyncSetUp(self):\n        self.ledger = Ledger({\n            'db': Database(':memory:'),\n            'headers': Headers(':memory:'),\n        })\n        await self.ledger.db.open()\n\n    async def asyncTearDown(self):\n        await self.ledger.db.close()\n\n    def estimates(self, *args):\n        txos = args[0] if isinstance(args[0], (GeneratorType, list)) else args\n        return [txo.get_estimator(self.ledger) for txo in txos]\n\n\nclass TestCoinSelectionTests(BaseSelectionTestCase):\n\n    def test_empty_coins(self):\n        self.assertListEqual(CoinSelector(0, 0).select([]), [])\n\n    def test_skip_binary_search_if_total_not_enough(self):\n        fee = utxo(CENT).get_estimator(self.ledger).fee\n        big_pool = self.estimates(utxo(CENT+fee) for _ in range(100))\n        selector = CoinSelector(101 * CENT, 0)\n        self.assertListEqual(selector.select(big_pool), [])\n        self.assertEqual(selector.tries, 0)  # Never tried.\n        # check happy path\n        selector = CoinSelector(100 * CENT, 0)\n        self.assertEqual(len(selector.select(big_pool)), 100)\n        self.assertEqual(selector.tries, 201)\n\n    def test_exact_match(self):\n        fee = utxo(CENT).get_estimator(self.ledger).fee\n        utxo_pool = self.estimates(\n            utxo(CENT + fee),\n            utxo(CENT),\n            utxo(CENT - fee)\n        )\n        selector = CoinSelector(CENT, 0)\n        match = selector.select(utxo_pool)\n        self.assertListEqual([CENT + fee], [c.txo.amount for c in match])\n        self.assertTrue(selector.exact_match)\n\n    def test_random_draw(self):\n        utxo_pool = self.estimates(\n            utxo(2 * CENT),\n            utxo(3 * CENT),\n            utxo(4 * CENT)\n        )\n        selector = CoinSelector(CENT, 0, '\\x00')\n        match = selector.select(utxo_pool)\n        self.assertListEqual([2 * CENT], [c.txo.amount for c in match])\n        self.assertFalse(selector.exact_match)\n\n    def test_pick(self):\n        utxo_pool = self.estimates(\n            utxo(1*CENT),\n            utxo(1*CENT),\n            utxo(3*CENT),\n            utxo(5*CENT),\n            utxo(10*CENT),\n        )\n        selector = CoinSelector(3*CENT, 0)\n        match = selector.select(utxo_pool)\n        self.assertListEqual([5*CENT], [c.txo.amount for c in match])\n\n    def test_confirmed_strategies(self):\n        utxo_pool = self.estimates(\n            utxo(11*CENT, height=5),\n            utxo(11*CENT, height=0),\n            utxo(11*CENT, height=-2),\n            utxo(11*CENT, height=5),\n        )\n\n        match = CoinSelector(20*CENT, 0).select(utxo_pool, \"only_confirmed\")\n        self.assertListEqual([5, 5], [c.txo.tx_ref.height for c in match])\n        match = CoinSelector(25*CENT, 0).select(utxo_pool, \"only_confirmed\")\n        self.assertListEqual([], [c.txo.tx_ref.height for c in match])\n\n        match = CoinSelector(20*CENT, 0).select(utxo_pool, \"prefer_confirmed\")\n        self.assertListEqual([5, 5], [c.txo.tx_ref.height for c in match])\n        match = CoinSelector(25*CENT, 0, '\\x00').select(utxo_pool, \"prefer_confirmed\")\n        self.assertListEqual([5, 0, -2], [c.txo.tx_ref.height for c in match])\n\n\nclass TestOfficialBitcoinCoinSelectionTests(BaseSelectionTestCase):\n\n    #       Bitcoin implementation:\n    #       https://github.com/bitcoin/bitcoin/blob/master/src/wallet/coinselection.cpp\n    #\n    #       Bitcoin implementation tests:\n    #       https://github.com/bitcoin/bitcoin/blob/master/src/wallet/test/coinselector_tests.cpp\n    #\n    #       Branch and Bound coin selection white paper:\n    #       https://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf\n\n    def make_hard_case(self, utxos):\n        target = 0\n        utxo_pool = []\n        for i in range(utxos):\n            amount = 1 << (utxos+i)\n            target += amount\n            utxo_pool.append(utxo(amount))\n            utxo_pool.append(utxo(amount + (1 << (utxos-1-i))))\n        return self.estimates(utxo_pool), target\n\n    def test_branch_and_bound_coin_selection(self):\n        self.ledger.fee_per_byte = 0\n\n        utxo_pool = self.estimates(\n            utxo(1 * CENT),\n            utxo(2 * CENT),\n            utxo(3 * CENT),\n            utxo(4 * CENT)\n        )\n\n        # Select 1 Cent\n        self.assertListEqual([1 * CENT], search(utxo_pool, 1 * CENT, 0.5 * CENT))\n\n        # Select 2 Cent\n        self.assertListEqual([2 * CENT], search(utxo_pool, 2 * CENT, 0.5 * CENT))\n\n        # Select 5 Cent\n        self.assertListEqual([3 * CENT, 2 * CENT], search(utxo_pool, 5 * CENT, 0.5 * CENT))\n\n        # Select 11 Cent, not possible\n        self.assertListEqual([], search(utxo_pool, 11 * CENT, 0.5 * CENT))\n\n        # Select 10 Cent\n        utxo_pool += self.estimates(utxo(5 * CENT))\n        self.assertListEqual(\n            [4 * CENT, 3 * CENT, 2 * CENT, 1 * CENT],\n            search(utxo_pool, 10 * CENT, 0.5 * CENT)\n        )\n\n        # Negative effective value\n        # Select 10 Cent but have 1 Cent not be possible because too small\n        # TODO: bitcoin has [5, 3, 2]\n        self.assertListEqual(\n            [4 * CENT, 3 * CENT, 2 * CENT, 1 * CENT],\n            search(utxo_pool, 10 * CENT, 5000)\n        )\n\n        # Select 0.25 Cent, not possible\n        self.assertListEqual(search(utxo_pool, 0.25 * CENT, 0.5 * CENT), [])\n\n        # Iteration exhaustion test\n        utxo_pool, target = self.make_hard_case(17)\n        selector = CoinSelector(target, 0)\n        self.assertListEqual(selector.select(utxo_pool, 'branch_and_bound'), [])\n        self.assertEqual(selector.tries, MAXIMUM_TRIES)  # Should exhaust\n        utxo_pool, target = self.make_hard_case(14)\n        self.assertIsNotNone(search(utxo_pool, target, 0))  # Should not exhaust\n\n        # Test same value early bailout optimization\n        utxo_pool = self.estimates([\n            utxo(7 * CENT),\n            utxo(7 * CENT),\n            utxo(7 * CENT),\n            utxo(7 * CENT),\n            utxo(2 * CENT)\n        ] + [utxo(5 * CENT)]*50000)\n        self.assertListEqual(\n            [7 * CENT, 7 * CENT, 7 * CENT, 7 * CENT, 2 * CENT],\n            search(utxo_pool, 30 * CENT, 5000)\n        )\n\n        # Select 1 Cent with pool of only greater than 5 Cent\n        utxo_pool = self.estimates(utxo(i * CENT) for i in range(5, 21))\n        for _ in range(100):\n            self.assertListEqual(search(utxo_pool, 1 * CENT, 2 * CENT), [])\n"
  },
  {
    "path": "tests/unit/wallet/test_database.py",
    "content": "import sys\nimport os\nimport unittest\nimport sqlite3\nimport tempfile\nimport asyncio\nfrom concurrent.futures.thread import ThreadPoolExecutor\n\nfrom lbry.wallet import (\n    Wallet, Account, Ledger, Database, Headers, Transaction, Input\n)\nfrom lbry.wallet.constants import COIN\nfrom lbry.wallet.database import query, interpolate, constraints_to_sql, AIOSQLite\nfrom lbry.crypto.hash import sha256\nfrom lbry.testcase import AsyncioTestCase\n\nfrom tests.unit.wallet.test_transaction import get_output, NULL_HASH\n\n\nclass TestAIOSQLite(AsyncioTestCase):\n    async def asyncSetUp(self):\n        self.db = await AIOSQLite.connect(':memory:')\n        await self.db.executescript(\"\"\"\n        pragma foreign_keys=on;\n        create table parent (id integer primary key, name);\n        create table child  (id integer primary key, parent_id references parent);\n        \"\"\")\n        await self.db.execute(\"insert into parent values (1, 'test')\")\n        await self.db.execute(\"insert into child values (2, 1)\")\n\n    @staticmethod\n    def delete_item(transaction):\n        transaction.execute('delete from parent where id=1')\n\n    async def test_foreign_keys_integrity_error(self):\n        self.assertListEqual([(1, 'test')], await self.db.execute_fetchall(\"select * from parent\"))\n\n        with self.assertRaises(sqlite3.IntegrityError):\n            await self.db.run(self.delete_item)\n        self.assertListEqual([(1, 'test')], await self.db.execute_fetchall(\"select * from parent\"))\n\n        await self.db.executescript(\"pragma foreign_keys=off;\")\n\n        await self.db.run(self.delete_item)\n        self.assertListEqual([], await self.db.execute_fetchall(\"select * from parent\"))\n\n    async def test_run_without_foreign_keys(self):\n        self.assertListEqual([(1, 'test')], await self.db.execute_fetchall(\"select * from parent\"))\n        await self.db.run_with_foreign_keys_disabled(self.delete_item)\n        self.assertListEqual([], await self.db.execute_fetchall(\"select * from parent\"))\n\n    async def test_integrity_error_when_foreign_keys_disabled_and_skipped(self):\n        await self.db.executescript(\"pragma foreign_keys=off;\")\n        self.assertListEqual([(1, 'test')], await self.db.execute_fetchall(\"select * from parent\"))\n        with self.assertRaises(sqlite3.IntegrityError):\n            await self.db.run_with_foreign_keys_disabled(self.delete_item)\n        self.assertListEqual([(1, 'test')], await self.db.execute_fetchall(\"select * from parent\"))\n\n\nclass TestQueryBuilder(unittest.TestCase):\n\n    def test_dot(self):\n        self.assertTupleEqual(\n            constraints_to_sql({'txo.position': 18}),\n            ('txo.position = :txo_position0', {'txo_position0': 18})\n        )\n        self.assertTupleEqual(\n            constraints_to_sql({'txo.position#6': 18}),\n            ('txo.position = :txo_position6', {'txo_position6': 18})\n        )\n\n    def test_any(self):\n        self.assertTupleEqual(\n            constraints_to_sql({\n                'ages__any': {\n                    'txo.age__gt': 18,\n                    'txo.age__lt': 38\n                }\n            }),\n            ('(txo.age > :ages__any0_txo_age__gt0 OR txo.age < :ages__any0_txo_age__lt0)', {\n                'ages__any0_txo_age__gt0': 18,\n                'ages__any0_txo_age__lt0': 38\n            })\n        )\n\n    def test_in(self):\n        self.assertTupleEqual(\n            constraints_to_sql({'txo.age__in#2': [18, 38]}),\n            ('txo.age IN (:txo_age__in2_0, :txo_age__in2_1)', {\n                'txo_age__in2_0': 18,\n                'txo_age__in2_1': 38\n            })\n        )\n        self.assertTupleEqual(\n            constraints_to_sql({'txo.name__in': ('abc123', 'def456')}),\n            ('txo.name IN (:txo_name__in0_0, :txo_name__in0_1)', {\n                'txo_name__in0_0': 'abc123',\n                'txo_name__in0_1': 'def456'\n            })\n        )\n        self.assertTupleEqual(\n            constraints_to_sql({'txo.name__in': {'abc123'}}),\n            ('txo.name = :txo_name__in0', {\n                'txo_name__in0': 'abc123',\n            })\n        )\n        self.assertTupleEqual(\n            constraints_to_sql({'txo.age__in': 'SELECT age from ages_table'}),\n            ('txo.age IN (SELECT age from ages_table)', {})\n        )\n\n    def test_not_in(self):\n        self.assertTupleEqual(\n            constraints_to_sql({'txo.age__not_in': [18, 38]}),\n            ('txo.age NOT IN (:txo_age__not_in0_0, :txo_age__not_in0_1)', {\n                'txo_age__not_in0_0': 18,\n                'txo_age__not_in0_1': 38\n            })\n        )\n        self.assertTupleEqual(\n            constraints_to_sql({'txo.name__not_in': ('abc123', 'def456')}),\n            ('txo.name NOT IN (:txo_name__not_in0_0, :txo_name__not_in0_1)', {\n                'txo_name__not_in0_0': 'abc123',\n                'txo_name__not_in0_1': 'def456'\n            })\n        )\n        self.assertTupleEqual(\n            constraints_to_sql({'txo.name__not_in': ('abc123',)}),\n            ('txo.name != :txo_name__not_in0', {\n                'txo_name__not_in0': 'abc123',\n            })\n        )\n        self.assertTupleEqual(\n            constraints_to_sql({'txo.age__not_in': 'SELECT age from ages_table'}),\n            ('txo.age NOT IN (SELECT age from ages_table)', {})\n        )\n\n    def test_in_invalid(self):\n        with self.assertRaisesRegex(ValueError, 'list, set or string'):\n            constraints_to_sql({'ages__in': 9})\n\n    def test_query(self):\n        self.assertTupleEqual(\n            query(\"select * from foo\"),\n            (\"select * from foo\", {})\n        )\n        self.assertTupleEqual(\n            query(\n                \"select * from foo\",\n                a__not='b', b__in='select * from blah where c=:$c',\n                d__any={'one__like': 'o', 'two': 2}, limit=10, order_by='b', **{'$c': 3}),\n            (\n                \"select * from foo WHERE a != :a__not0 AND \"\n                \"b IN (select * from blah where c=:$c) AND \"\n                \"(one LIKE :d__any0_one__like0 OR two = :d__any0_two0) ORDER BY b LIMIT 10\",\n                {'a__not0': 'b', 'd__any0_one__like0': 'o', 'd__any0_two0': 2, '$c': 3}\n            )\n        )\n\n    def test_query_order_by(self):\n        self.assertTupleEqual(\n            query(\"select * from foo\", order_by='foo'),\n            (\"select * from foo ORDER BY foo\", {})\n        )\n        self.assertTupleEqual(\n            query(\"select * from foo\", order_by=['foo', 'bar']),\n            (\"select * from foo ORDER BY foo, bar\", {})\n        )\n        with self.assertRaisesRegex(ValueError, 'order_by must be string or list'):\n            query(\"select * from foo\", order_by={'foo': 'bar'})\n\n    def test_query_limit_offset(self):\n        self.assertTupleEqual(\n            query(\"select * from foo\", limit=10),\n            (\"select * from foo LIMIT 10\", {})\n        )\n        self.assertTupleEqual(\n            query(\"select * from foo\", offset=10),\n            (\"select * from foo OFFSET 10\", {})\n        )\n        self.assertTupleEqual(\n            query(\"select * from foo\", limit=20, offset=10),\n            (\"select * from foo LIMIT 20 OFFSET 10\", {})\n        )\n\n    def test_query_interpolation(self):\n        self.maxDiff = None\n        # tests that interpolation replaces longer keys first\n        self.assertEqual(\n            interpolate(*query(\n                \"select * from foo\",\n                a__not='b', b__in='select * from blah where c=:$c',\n                d__any={'one__like': 'o', 'two': 2},\n                a0=3, a00=1, a00a=2, a00aa=4,  # <-- breaks without correct interpolation key order\n                ahash=sha256(b'hello world'),\n                limit=10, order_by='b', **{'$c': 3})\n            ),\n            \"select * from foo WHERE a != 'b' AND \"\n            \"b IN (select * from blah where c=3) AND \"\n            \"(one LIKE 'o' OR two = 2) AND \"\n            \"a0 = 3 AND a00 = 1 AND a00a = 2 AND a00aa = 4 \"\n            \"AND ahash = X'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' \"\n            \"ORDER BY b LIMIT 10\"\n        )\n\n\nclass TestQueries(AsyncioTestCase):\n\n    async def asyncSetUp(self):\n        self.ledger = Ledger({\n            'db': Database(':memory:'),\n            'headers': Headers(':memory:')\n        })\n        await self.ledger.headers.open()\n        self.wallet = Wallet()\n        await self.ledger.db.open()\n\n    async def asyncTearDown(self):\n        await self.ledger.db.close()\n\n    async def create_account(self, wallet=None):\n        account = Account.generate(self.ledger, wallet or self.wallet)\n        await account.ensure_address_gap()\n        return account\n\n    async def create_tx_from_nothing(self, my_account, height):\n        to_address = await my_account.receiving.get_or_create_usable_address()\n        to_hash = Ledger.address_to_hash160(to_address)\n        tx = Transaction(height=height, is_verified=True) \\\n            .add_inputs([self.txi(self.txo(1, sha256(str(height).encode())))]) \\\n            .add_outputs([self.txo(1, to_hash)])\n        await self.ledger.db.insert_transaction(tx)\n        await self.ledger.db.save_transaction_io(tx, to_address, to_hash, '')\n        return tx\n\n    async def create_tx_from_txo(self, txo, to_account, height):\n        from_hash = txo.script.values['pubkey_hash']\n        from_address = self.ledger.hash160_to_address(from_hash)\n        to_address = await to_account.receiving.get_or_create_usable_address()\n        to_hash = Ledger.address_to_hash160(to_address)\n        tx = Transaction(height=height, is_verified=True) \\\n            .add_inputs([self.txi(txo)]) \\\n            .add_outputs([self.txo(1, to_hash)])\n        await self.ledger.db.insert_transaction(tx)\n        await self.ledger.db.save_transaction_io(tx, from_address, from_hash, '')\n        await self.ledger.db.save_transaction_io(tx, to_address, to_hash, '')\n        return tx\n\n    async def create_tx_to_nowhere(self, txo, height):\n        from_hash = txo.script.values['pubkey_hash']\n        from_address = self.ledger.hash160_to_address(from_hash)\n        to_hash = NULL_HASH\n        tx = Transaction(height=height, is_verified=True) \\\n            .add_inputs([self.txi(txo)]) \\\n            .add_outputs([self.txo(1, to_hash)])\n        await self.ledger.db.insert_transaction(tx)\n        await self.ledger.db.save_transaction_io(tx, from_address, from_hash, '')\n        return tx\n\n    def txo(self, amount, address):\n        return get_output(int(amount*COIN), address)\n\n    def txi(self, txo):\n        return Input.spend(txo)\n\n    async def test_large_tx_doesnt_hit_variable_limits(self):\n        # SQLite is usually compiled with 999 variables limit: https://www.sqlite.org/limits.html\n        # This can be removed when there is a better way. See: https://github.com/lbryio/lbry-sdk/issues/2281\n        fetchall = self.ledger.db.db.execute_fetchall\n\n        def check_parameters_length(sql, parameters, read_only=False):\n            self.assertLess(len(parameters or []), 999)\n            return fetchall(sql, parameters, read_only)\n\n        self.ledger.db.db.execute_fetchall = check_parameters_length\n        account = await self.create_account()\n        tx = await self.create_tx_from_nothing(account, 0)\n        for height in range(1, 1200):\n            tx = await self.create_tx_from_txo(tx.outputs[0], account, height=height)\n        variable_limit = self.ledger.db.MAX_QUERY_VARIABLES\n        for limit in range(variable_limit - 2, variable_limit + 2):\n            txs = await self.ledger.get_transactions(\n                accounts=self.wallet.accounts, limit=limit, order_by='height asc')\n            self.assertEqual(len(txs), limit)\n            inputs, outputs, last_tx = set(), set(), txs[0]\n            for tx in txs[1:]:\n                self.assertEqual(len(tx.inputs), 1)\n                self.assertEqual(tx.inputs[0].txo_ref.tx_ref.id, last_tx.id)\n                self.assertEqual(len(tx.outputs), 1)\n                last_tx = tx\n\n    async def test_queries(self):\n        wallet1 = Wallet()\n        account1 = await self.create_account(wallet1)\n        self.assertEqual(26, await self.ledger.db.get_address_count(accounts=[account1]))\n        wallet2 = Wallet()\n        account2 = await self.create_account(wallet2)\n        account3 = await self.create_account(wallet2)\n        self.assertEqual(26, await self.ledger.db.get_address_count(accounts=[account2]))\n\n        self.assertEqual(0, await self.ledger.db.get_transaction_count(accounts=[account1, account2, account3]))\n        self.assertEqual(0, await self.ledger.db.get_utxo_count())\n        self.assertListEqual([], await self.ledger.db.get_utxos())\n        self.assertEqual(0, await self.ledger.db.get_txo_count())\n        self.assertEqual(0, await self.ledger.db.get_balance(wallet=wallet1))\n        self.assertEqual(0, await self.ledger.db.get_balance(wallet=wallet2))\n        self.assertEqual(0, await self.ledger.db.get_balance(accounts=[account1]))\n        self.assertEqual(0, await self.ledger.db.get_balance(accounts=[account2]))\n        self.assertEqual(0, await self.ledger.db.get_balance(accounts=[account3]))\n\n        tx1 = await self.create_tx_from_nothing(account1, 1)\n        self.assertEqual(1, await self.ledger.db.get_transaction_count(accounts=[account1]))\n        self.assertEqual(0, await self.ledger.db.get_transaction_count(accounts=[account2]))\n        self.assertEqual(1, await self.ledger.db.get_utxo_count(accounts=[account1]))\n        self.assertEqual(1, await self.ledger.db.get_txo_count(accounts=[account1]))\n        self.assertEqual(0, await self.ledger.db.get_txo_count(accounts=[account2]))\n        self.assertEqual(10**8, await self.ledger.db.get_balance(wallet=wallet1))\n        self.assertEqual(0, await self.ledger.db.get_balance(wallet=wallet2))\n        self.assertEqual(10**8, await self.ledger.db.get_balance(accounts=[account1]))\n        self.assertEqual(0, await self.ledger.db.get_balance(accounts=[account2]))\n        self.assertEqual(0, await self.ledger.db.get_balance(accounts=[account3]))\n\n        tx2 = await self.create_tx_from_txo(tx1.outputs[0], account2, 2)\n        tx2b = await self.create_tx_from_nothing(account3, 2)\n        self.assertEqual(2, await self.ledger.db.get_transaction_count(accounts=[account1]))\n        self.assertEqual(1, await self.ledger.db.get_transaction_count(accounts=[account2]))\n        self.assertEqual(1, await self.ledger.db.get_transaction_count(accounts=[account3]))\n        self.assertEqual(0, await self.ledger.db.get_utxo_count(accounts=[account1]))\n        self.assertEqual(1, await self.ledger.db.get_txo_count(accounts=[account1]))\n        self.assertEqual(1, await self.ledger.db.get_utxo_count(accounts=[account2]))\n        self.assertEqual(1, await self.ledger.db.get_txo_count(accounts=[account2]))\n        self.assertEqual(1, await self.ledger.db.get_utxo_count(accounts=[account3]))\n        self.assertEqual(1, await self.ledger.db.get_txo_count(accounts=[account3]))\n        self.assertEqual(0, await self.ledger.db.get_balance(wallet=wallet1))\n        self.assertEqual(10**8+10**8, await self.ledger.db.get_balance(wallet=wallet2))\n        self.assertEqual(0, await self.ledger.db.get_balance(accounts=[account1]))\n        self.assertEqual(10**8, await self.ledger.db.get_balance(accounts=[account2]))\n        self.assertEqual(10**8, await self.ledger.db.get_balance(accounts=[account3]))\n\n        tx3 = await self.create_tx_to_nowhere(tx2.outputs[0], 3)\n        self.assertEqual(2, await self.ledger.db.get_transaction_count(accounts=[account1]))\n        self.assertEqual(2, await self.ledger.db.get_transaction_count(accounts=[account2]))\n        self.assertEqual(0, await self.ledger.db.get_utxo_count(accounts=[account1]))\n        self.assertEqual(1, await self.ledger.db.get_txo_count(accounts=[account1]))\n        self.assertEqual(0, await self.ledger.db.get_utxo_count(accounts=[account2]))\n        self.assertEqual(1, await self.ledger.db.get_txo_count(accounts=[account2]))\n        self.assertEqual(0, await self.ledger.db.get_balance(wallet=wallet1))\n        self.assertEqual(10**8, await self.ledger.db.get_balance(wallet=wallet2))\n        self.assertEqual(0, await self.ledger.db.get_balance(accounts=[account1]))\n        self.assertEqual(0, await self.ledger.db.get_balance(accounts=[account2]))\n        self.assertEqual(10**8, await self.ledger.db.get_balance(accounts=[account3]))\n\n        txs = await self.ledger.db.get_transactions(accounts=[account1, account2])\n        self.assertListEqual([tx3.id, tx2.id, tx1.id], [tx.id for tx in txs])\n        self.assertListEqual([3, 2, 1], [tx.height for tx in txs])\n\n        txs = await self.ledger.db.get_transactions(wallet=wallet1, accounts=wallet1.accounts, include_is_my_output=True)\n        self.assertListEqual([tx2.id, tx1.id], [tx.id for tx in txs])\n        self.assertEqual(txs[0].inputs[0].is_my_input, True)\n        self.assertEqual(txs[0].outputs[0].is_my_output, False)\n        self.assertEqual(txs[1].inputs[0].is_my_input, False)\n        self.assertEqual(txs[1].outputs[0].is_my_output, True)\n\n        txs = await self.ledger.db.get_transactions(wallet=wallet2, accounts=[account2], include_is_my_output=True)\n        self.assertListEqual([tx3.id, tx2.id], [tx.id for tx in txs])\n        self.assertEqual(txs[0].inputs[0].is_my_input, True)\n        self.assertEqual(txs[0].outputs[0].is_my_output, False)\n        self.assertEqual(txs[1].inputs[0].is_my_input, False)\n        self.assertEqual(txs[1].outputs[0].is_my_output, True)\n        self.assertEqual(2, await self.ledger.db.get_transaction_count(accounts=[account2]))\n\n        tx = await self.ledger.db.get_transaction(txid=tx2.id)\n        self.assertEqual(tx.id, tx2.id)\n        self.assertIsNone(tx.inputs[0].is_my_input)\n        self.assertIsNone(tx.outputs[0].is_my_output)\n        tx = await self.ledger.db.get_transaction(wallet=wallet1, txid=tx2.id, include_is_my_output=True)\n        self.assertTrue(tx.inputs[0].is_my_input)\n        self.assertFalse(tx.outputs[0].is_my_output)\n        tx = await self.ledger.db.get_transaction(wallet=wallet2, txid=tx2.id, include_is_my_output=True)\n        self.assertFalse(tx.inputs[0].is_my_input)\n        self.assertTrue(tx.outputs[0].is_my_output)\n\n        # height 0 sorted to the top with the rest in descending order\n        tx4 = await self.create_tx_from_nothing(account1, 0)\n        txos = await self.ledger.db.get_txos()\n        self.assertListEqual([0, 3, 2, 2, 1], [txo.tx_ref.height for txo in txos])\n        self.assertListEqual([tx4.id, tx3.id, tx2.id, tx2b.id, tx1.id], [txo.tx_ref.id for txo in txos])\n        txs = await self.ledger.db.get_transactions(accounts=[account1, account2])\n        self.assertListEqual([0, 3, 2, 1], [tx.height for tx in txs])\n        self.assertListEqual([tx4.id, tx3.id, tx2.id, tx1.id], [tx.id for tx in txs])\n\n    async def test_empty_history(self):\n        self.assertEqual((None, []), await self.ledger.get_local_status_and_history(''))\n\n\nclass TestUpgrade(AsyncioTestCase):\n\n    def setUp(self) -> None:\n        self.path = tempfile.mktemp()\n\n    def tearDown(self) -> None:\n        os.remove(self.path)\n\n    def get_version(self):\n        with sqlite3.connect(self.path) as conn:\n            versions = conn.execute('select version from version').fetchall()\n            assert len(versions) == 1\n            return versions[0][0]\n\n    def get_tables(self):\n        with sqlite3.connect(self.path) as conn:\n            sql = \"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;\"\n            return [col[0] for col in conn.execute(sql).fetchall()]\n\n    def add_address(self, address):\n        with sqlite3.connect(self.path) as conn:\n            conn.execute(\"\"\"\n            INSERT INTO account_address (address, account, chain, n, pubkey, chain_code, depth)\n            VALUES (?, 'account1', 0, 0, 'pubkey', 'chain_code', 0)\n            \"\"\", (address,))\n\n    def get_addresses(self):\n        with sqlite3.connect(self.path) as conn:\n            sql = \"SELECT address FROM account_address ORDER BY address;\"\n            return [col[0] for col in conn.execute(sql).fetchall()]\n\n    async def test_reset_on_version_change(self):\n        self.ledger = Ledger({\n            'db': Database(self.path),\n            'headers': Headers(':memory:')\n        })\n\n        # initial open, pre-version enabled db\n        self.ledger.db.SCHEMA_VERSION = None\n        self.assertListEqual(self.get_tables(), [])\n        await self.ledger.db.open()\n        self.assertEqual(self.get_tables(), ['account_address', 'pubkey_address', 'tx', 'txi', 'txo'])\n        self.assertListEqual(self.get_addresses(), [])\n        self.add_address('address1')\n        await self.ledger.db.close()\n\n        # initial open after version enabled\n        self.ledger.db.SCHEMA_VERSION = '1.0'\n        await self.ledger.db.open()\n        self.assertEqual(self.get_version(), '1.0')\n        self.assertListEqual(self.get_tables(), ['account_address', 'pubkey_address', 'tx', 'txi', 'txo', 'version'])\n        self.assertListEqual(self.get_addresses(), [])  # address1 deleted during version upgrade\n        self.add_address('address2')\n        await self.ledger.db.close()\n\n        # nothing changes\n        self.assertEqual(self.get_version(), '1.0')\n        self.assertListEqual(self.get_tables(), ['account_address', 'pubkey_address', 'tx', 'txi', 'txo', 'version'])\n        await self.ledger.db.open()\n        self.assertEqual(self.get_version(), '1.0')\n        self.assertListEqual(self.get_tables(), ['account_address', 'pubkey_address', 'tx', 'txi', 'txo', 'version'])\n        self.assertListEqual(self.get_addresses(), ['address2'])\n        await self.ledger.db.close()\n\n        # upgrade version, database reset\n        self.ledger.db.SCHEMA_VERSION = '1.1'\n        self.ledger.db.CREATE_TABLES_QUERY += \"\"\"\n        create table if not exists foo (bar text);\n        \"\"\"\n        await self.ledger.db.open()\n        self.assertEqual(self.get_version(), '1.1')\n        self.assertListEqual(self.get_tables(), ['account_address', 'foo', 'pubkey_address', 'tx', 'txi', 'txo', 'version'])\n        self.assertListEqual(self.get_addresses(), [])  # all tables got reset\n        await self.ledger.db.close()\n\n\nclass TestSQLiteRace(AsyncioTestCase):\n    max_misuse_attempts = 120000\n\n    def setup_db(self):\n        self.db = sqlite3.connect(\":memory:\", isolation_level=None)\n        self.db.executescript(\n            \"create table test1 (id text primary key not null, val text);\\n\" +\n            \"create table test2 (id text primary key not null, val text);\\n\" +\n            \"\\n\".join(f\"insert into test1 values ({v}, NULL);\" for v in range(1000))\n        )\n\n    async def asyncSetUp(self):\n        self.executor = ThreadPoolExecutor(1)\n        await self.loop.run_in_executor(self.executor, self.setup_db)\n\n    async def asyncTearDown(self):\n        await self.loop.run_in_executor(self.executor, self.db.close)\n        self.executor.shutdown()\n\n    async def test_binding_param_0_error(self):\n        # test real param 0 binding errors\n\n        for supported_type in [str, int, bytes]:\n            await self.loop.run_in_executor(\n                self.executor, self.db.executemany, \"insert into test2 values (?, NULL)\",\n                [(supported_type(1), ), (supported_type(2), )]\n            )\n            await self.loop.run_in_executor(\n                self.executor, self.db.execute, \"delete from test2 where id in (1, 2)\"\n            )\n        for unsupported_type in [lambda x: (x, ), lambda x: [x], lambda x: {x}]:\n            try:\n                await self.loop.run_in_executor(\n                    self.executor, self.db.executemany, \"insert into test2 (id, val) values (?, NULL)\",\n                    [(unsupported_type(1), ), (unsupported_type(2), )]\n                )\n                self.assertTrue(False)\n            except sqlite3.InterfaceError as err:\n                self.assertEqual(str(err), \"Error binding parameter 0 - probably unsupported type.\")\n\n    async def test_unhandled_sqlite_misuse(self):\n        # test SQLITE_MISUSE being incorrectly raised as a param 0 binding error\n        attempts = 0\n        python_version = sys.version.split('\\n')[0].rstrip(' ')\n\n        try:\n            while attempts < self.max_misuse_attempts:\n                f1 = asyncio.wrap_future(\n                    self.loop.run_in_executor(\n                        self.executor, self.db.executemany, \"update test1 set val='derp' where id=?\",\n                        ((str(i),) for i in range(2))\n                    )\n                )\n                f2 = asyncio.wrap_future(\n                    self.loop.run_in_executor(\n                        self.executor, self.db.executemany, \"update test2 set val='derp' where id=?\",\n                        ((str(i),) for i in range(2))\n                    )\n                )\n                attempts += 1\n                await asyncio.gather(f1, f2)\n            print(f\"\\nsqlite3 {sqlite3.version}/python {python_version} \"\n                  f\"did not raise SQLITE_MISUSE within {attempts} attempts of the race condition\")\n            self.assertTrue(False, 'this test failing means either the sqlite race conditions '\n                                   'have been fixed in cpython or the test max_attempts needs to be increased')\n        except sqlite3.InterfaceError as err:\n            self.assertEqual(str(err), \"Error binding parameter 0 - probably unsupported type.\")\n        print(f\"\\nsqlite3 {sqlite3.version}/python {python_version} raised SQLITE_MISUSE \"\n              f\"after {attempts} attempts of the race condition\")\n\n    @unittest.SkipTest\n    async def test_fetchall_prevents_sqlite_misuse(self):\n        # test that calling fetchall sufficiently avoids the race\n        attempts = 0\n\n        def executemany_fetchall(query, params):\n            self.db.executemany(query, params).fetchall()\n\n        while attempts < self.max_misuse_attempts:\n            f1 = asyncio.wrap_future(\n                self.loop.run_in_executor(\n                    self.executor, executemany_fetchall, \"update test1 set val='derp' where id=?\",\n                    ((str(i),) for i in range(2))\n                )\n            )\n            f2 = asyncio.wrap_future(\n                self.loop.run_in_executor(\n                    self.executor, executemany_fetchall, \"update test2 set val='derp' where id=?\",\n                    ((str(i),) for i in range(2))\n                )\n            )\n            attempts += 1\n            await asyncio.gather(f1, f2)"
  },
  {
    "path": "tests/unit/wallet/test_dewies.py",
    "content": "import unittest\n\nfrom lbry.wallet.dewies import lbc_to_dewies as l2d, dewies_to_lbc as d2l\n\n\nclass TestDeweyConversion(unittest.TestCase):\n\n    def test_good_output(self):\n        self.assertEqual(d2l(1), \"0.00000001\")\n        self.assertEqual(d2l(10**7), \"0.1\")\n        self.assertEqual(d2l(2*10**8), \"2.0\")\n        self.assertEqual(d2l(2*10**17), \"2000000000.0\")\n\n    def test_good_input(self):\n        self.assertEqual(l2d(\"0.00000001\"), 1)\n        self.assertEqual(l2d(\"0.1\"), 10**7)\n        self.assertEqual(l2d(\"1.0\"), 10**8)\n        self.assertEqual(l2d(\"2.00000000\"), 2*10**8)\n        self.assertEqual(l2d(\"2000000000.0\"), 2*10**17)\n\n    def test_bad_input(self):\n        with self.assertRaises(ValueError):\n            l2d(\"1\")\n        with self.assertRaises(ValueError):\n            l2d(\"-1.0\")\n        with self.assertRaises(ValueError):\n            l2d(\"10000000000.0\")\n        with self.assertRaises(ValueError):\n            l2d(\"1.000000000\")\n        with self.assertRaises(ValueError):\n            l2d(\"-0\")\n        with self.assertRaises(ValueError):\n            l2d(\"1\")\n        with self.assertRaises(ValueError):\n            l2d(\".1\")\n        with self.assertRaises(ValueError):\n            l2d(\"1e-7\")\n"
  },
  {
    "path": "tests/unit/wallet/test_hash.py",
    "content": "from unittest import TestCase, mock\nfrom lbry.crypto.crypt import aes_decrypt, aes_encrypt, better_aes_decrypt, better_aes_encrypt\nfrom lbry.error import InvalidPasswordError\n\n\nclass TestAESEncryptDecrypt(TestCase):\n    message = 'The Times 03/Jan/2009 Chancellor on brink of second bailout for banks'\n    expected = 'ZmZmZmZmZmZmZmZmZmZmZjlrKptoKD+MFwDxcg3XtCD9qz8UWhEhq/TVJT5+Mtp2a8sE' \\\n               'CaO6WQj7fYsWGu2Hvbc0qYqxdN0HeTsiO+cZRo3eJISgr3F+rXFYi5oSBlD2'\n    password = 'bubblegum'\n\n    @mock.patch('os.urandom', side_effect=lambda i: b'd'*i)\n    def test_encrypt_iv_f(self, _):\n        self.assertEqual(\n            aes_encrypt(self.password, self.message),\n           'ZGRkZGRkZGRkZGRkZGRkZKBP/4pR+47hLHbHyvDJm9aRKDuoBdTG8SrFvHqfagK6Co1VrHUOd'\n           'oF+6PGSxru3+VR63ybkXLNM75s/qVw+dnKVAkI8OfoVnJvGRSc49e38'\n        )\n\n    @mock.patch('os.urandom', side_effect=lambda i: b'f'*i)\n    def test_encrypt_iv_d(self, _):\n        self.assertEqual(\n            aes_encrypt(self.password, self.message),\n           'ZmZmZmZmZmZmZmZmZmZmZjlrKptoKD+MFwDxcg3XtCD9qz8UWhEhq/TVJT5+Mtp2a8sE'\n           'CaO6WQj7fYsWGu2Hvbc0qYqxdN0HeTsiO+cZRo3eJISgr3F+rXFYi5oSBlD2'\n        )\n        self.assertTupleEqual(\n            aes_decrypt(self.password, self.expected),\n            (self.message, b'f' * 16)\n        )\n\n    def test_encrypt_decrypt(self):\n        self.assertEqual(\n            aes_decrypt('bubblegum', aes_encrypt('bubblegum', self.message))[0],\n            self.message\n        )\n\n    def test_decrypt_error(self):\n        with self.assertRaises(InvalidPasswordError):\n            aes_decrypt('notbubblegum', aes_encrypt('bubblegum', self.message))\n\n    def test_edge_case_invalid_password_valid_padding_invalid_unicode(self):\n        with self.assertRaises(InvalidPasswordError):\n            aes_decrypt(\n                'notbubblegum',\n                'gy3/mNq3FWB/xAXirOQnlAqQLuvhLGXZaeGBUIg1w6yY4PDLDT7BU83XOfBsJol'\n                'uWU5zEU4+upOFH35HDqyV8EMQhcKSufN9WkT1izEbFtweBUTK8nTSkV7NBppE1Jaz'\n            )\n\n    def test_better_encrypt_decrypt(self):\n        self.assertEqual(\n            b'valuable value',\n            better_aes_decrypt(\n                'super secret',\n                better_aes_encrypt('super secret', b'valuable value')))\n\n    @mock.patch('os.urandom', side_effect=lambda i: b'd'*i)\n    def test_better_decrypt_error(self, _):\n        with self.assertRaises(InvalidPasswordError):\n            better_aes_decrypt(\n                'super secret but wrong',\n                better_aes_encrypt('super secret', b'valuable value')\n            )\n\n    def test_edge_case_invalid_password_valid_everything(self):\n        value = b'czo4MTkyOjE2OjE6VrwsN8FSJlegxHVEQePoyjWT1k8yAXBCUbbGCFKcsNY='\n        self.assertEqual(b'valuable value', better_aes_decrypt('super secret', value))\n        self.assertNotEqual(b'valuable value', better_aes_decrypt('super secret but wrong', value))\n"
  },
  {
    "path": "tests/unit/wallet/test_headers.py",
    "content": "import os\nimport asyncio\nimport tempfile\nfrom binascii import unhexlify\n\nfrom lbry.wallet.util import ArithUint256\nfrom lbry.testcase import AsyncioTestCase\nfrom lbry.wallet.ledger import Headers as _Headers\n\n\nclass Headers(_Headers):\n    checkpoints = {}\n\n\ndef block_bytes(blocks):\n    return blocks * Headers.header_size\n\n\nclass TestHeaders(AsyncioTestCase):\n\n    async def test_deserialize(self):\n        self.maxDiff = None\n        h = Headers(':memory:')\n        await h.open()\n        await h.connect(0, HEADERS)\n        self.assertEqual(await h.get(0), {\n            'bits': 520159231,\n            'block_height': 0,\n            'claim_trie_root': b'0000000000000000000000000000000000000000000000000000000000000001',\n            'merkle_root': b'b8211c82c3d15bcd78bba57005b86fed515149a53a425eb592c07af99fe559cc',\n            'nonce': 1287,\n            'prev_block_hash': b'0000000000000000000000000000000000000000000000000000000000000000',\n            'timestamp': 1446058291,\n            'version': 1\n        })\n        self.assertEqual(await h.get(10), {\n            'bits': 509349720,\n            'block_height': 10,\n            'merkle_root': b'f4d8fded6a181d4a8a2817a0eb423cc0f414af29490004a620e66c35c498a554',\n            'claim_trie_root': b'0000000000000000000000000000000000000000000000000000000000000001',\n            'nonce': 75838,\n            'prev_block_hash': b'fdab1b38bcf236bc85b6bcd52fe8ec19bcb0b6c7352e913de05fa5a4e5ae8d55',\n            'timestamp': 1466646593,\n            'version': 536870912\n        })\n\n    async def test_connect_from_genesis(self):\n        headers = Headers(':memory:')\n        await headers.open()\n        self.assertEqual(headers.height, -1)\n        await headers.connect(0, HEADERS)\n        self.assertEqual(headers.height, 19)\n\n    async def test_connect_from_middle(self):\n        headers_temporary_file = tempfile.mktemp()\n        self.addCleanup(os.remove, headers_temporary_file)\n        with open(headers_temporary_file, 'w+b') as headers_file:\n            headers_file.write(HEADERS[:block_bytes(10)])\n        h = Headers(headers_temporary_file)\n        await h.open()\n        self.assertEqual(h.height, 9)\n        await h.connect(len(h), HEADERS[block_bytes(10):block_bytes(20)])\n        self.assertEqual(h.height, 19)\n\n    def test_target_calculation(self):\n        # see: https://github.com/lbryio/lbrycrd/blob/master/src/test/lbry_tests.cpp\n        # 1 test block 1 difficulty, should be a max retarget\n        self.assertEqual(\n            0x1f00e146,\n            Headers(':memory').get_next_block_target(\n                max_target=ArithUint256(Headers.max_target),\n                previous={'timestamp': 1386475638},\n                current={'timestamp': 1386475638, 'bits': 0x1f00ffff}\n            ).compact\n        )\n        # test max retarget (difficulty increase)\n        self.assertEqual(\n            0x1f008ccc,\n            Headers(':memory').get_next_block_target(\n                max_target=ArithUint256(Headers.max_target),\n                previous={'timestamp': 1386475638},\n                current={'timestamp': 1386475638, 'bits': 0x1f00a000}\n            ).compact\n        )\n        # test min retarget (difficulty decrease)\n        self.assertEqual(\n            0x1f00f000,\n            Headers(':memory').get_next_block_target(\n                max_target=ArithUint256(Headers.max_target),\n                previous={'timestamp': 1386475638},\n                current={'timestamp': 1386475638 + 60*20, 'bits': 0x1f00a000}\n            ).compact\n        )\n        # test to see if pow limit is not exceeded\n        self.assertEqual(\n            0x1f00ffff,\n            Headers(':memory').get_next_block_target(\n                max_target=ArithUint256(Headers.max_target),\n                previous={'timestamp': 1386475638},\n                current={'timestamp': 1386475638 + 600, 'bits': 0x1f00ffff}\n            ).compact\n        )\n\n    def test_get_proof_of_work_hash(self):\n        # see: https://github.com/lbryio/lbrycrd/blob/master/src/test/lbry_tests.cpp\n        self.assertEqual(\n            Headers.header_hash_to_pow_hash(Headers.hash_header(b\"test string\")),\n            b\"485f3920d48a0448034b0852d1489cfa475341176838c7d36896765221be35ce\"\n        )\n        self.assertEqual(\n            Headers.header_hash_to_pow_hash(Headers.hash_header(b\"a\"*70)),\n            b\"eb44af2f41e7c6522fb8be4773661be5baa430b8b2c3a670247e9ab060608b75\"\n        )\n        self.assertEqual(\n            Headers.header_hash_to_pow_hash(Headers.hash_header(b\"d\"*140)),\n            b\"74044747b7c1ff867eb09a84d026b02d8dc539fb6adcec3536f3dfa9266495d9\"\n        )\n\n    async def test_bounds(self):\n        headers = Headers(':memory:')\n        await headers.open()\n        await headers.connect(0, HEADERS)\n        self.assertEqual(19, headers.height)\n        with self.assertRaises(IndexError):\n            _ = await headers.get(3001)\n        with self.assertRaises(IndexError):\n            _ = await headers.get(-1)\n        self.assertIsNotNone(await headers.get(19))\n        self.assertIsNotNone(await headers.get(0))\n\n    async def test_repair(self):\n        headers = Headers(':memory:')\n        await headers.open()\n        await headers.connect(0, HEADERS[:block_bytes(11)])\n        self.assertEqual(10, headers.height)\n        await headers.repair()\n        self.assertEqual(10, headers.height)\n        # corrupt the middle of it\n        headers.io.seek(block_bytes(8))\n        headers.io.write(b\"wtf\")\n        await headers.repair()\n        self.assertEqual(7, headers.height)\n        self.assertEqual(8, len(headers))\n        # corrupt by appending\n        headers.io.seek(block_bytes(len(headers)))\n        headers.io.write(b\"appending\")\n        await headers.repair()\n        self.assertEqual(7, headers.height)\n        await headers.connect(len(headers), HEADERS[block_bytes(8):])\n        self.assertEqual(19, headers.height)\n        # verify from middle\n        await headers.repair(start_height=10)\n        self.assertEqual(19, headers.height)\n\n    async def test_do_not_estimate_unconfirmed(self):\n        headers = Headers(':memory:')\n        await headers.open()\n        self.assertIsNone(headers.estimated_timestamp(-1))\n        self.assertIsNone(headers.estimated_timestamp(0))\n        self.assertIsNotNone(headers.estimated_timestamp(1))\n\n    async def test_dont_estimate_whats_there(self):\n        headers = Headers(':memory:')\n        await headers.open()\n        estimated = headers.estimated_timestamp(10)\n        await headers.connect(0, HEADERS)\n        real_time = (await headers.get(10))['timestamp']\n        after_downloading_header_estimated = headers.estimated_timestamp(10)\n        self.assertNotEqual(estimated, after_downloading_header_estimated)\n        self.assertEqual(after_downloading_header_estimated, real_time)\n\n    async def test_misalignment_triggers_repair_on_open(self):\n        headers_temporary_file = tempfile.mktemp()\n        self.addCleanup(os.remove, headers_temporary_file)\n        with open(headers_temporary_file, 'w+b') as headers_file:\n            headers_file.write(HEADERS)\n        headers = Headers(headers_temporary_file)\n        with self.assertLogs(level='WARN') as cm:\n            await headers.open()\n            await headers.close()\n            self.assertEqual(cm.output, [])\n            with open(headers_temporary_file, 'w+b') as headers_file:\n                headers_file.seek(0)\n                headers_file.truncate()\n                headers_file.write(HEADERS[:block_bytes(10)])\n                headers_file.write(b'ops')\n                headers_file.write(HEADERS[block_bytes(10):])\n            await headers.open()\n            self.assertEqual(\n                cm.output, [\n                    'WARNING:lbry.wallet.header:Reader file size doesnt match header size. '\n                    'Repairing, might take a while.',\n                    'WARNING:lbry.wallet.header:Header file corrupted at height 9, truncating '\n                    'it.'\n                ]\n            )\n\n    async def test_concurrency(self):\n        BLOCKS = 19\n        headers_temporary_file = tempfile.mktemp()\n        headers = Headers(headers_temporary_file)\n        await headers.open()\n        self.addCleanup(os.remove, headers_temporary_file)\n        async def writer():\n            for block_index in range(BLOCKS):\n                await headers.connect(block_index, HEADERS[block_bytes(block_index):block_bytes(block_index + 1)])\n        async def reader():\n            for block_index in range(BLOCKS):\n                while len(headers) <= block_index:\n                    await asyncio.sleep(0.000001)\n                assert (await headers.get(block_index))['block_height'] == block_index\n        reader_task = asyncio.create_task(reader())\n        await writer()\n        await reader_task\n        await headers.close()\n\n\nHEADERS = unhexlify(\n    b'010000000000000000000000000000000000000000000000000000000000000000000000cc59e59ff97ac092b55e4'\n    b'23aa5495151ed6fb80570a5bb78cd5bd1c3821c21b801000000000000000000000000000000000000000000000000'\n    b'0000000000000033193156ffff001f070500000000002063f4346a4db34fdfce29a70f5e8d11f065f6b91602b7036'\n    b'c7f22f3a03b28899cba888e2f9c037f831046f8ad09f6d378f79c728d003b177a64d29621f481da5d010000000000'\n    b'00000000000000000000000000000000000000000000000000003c406b5746e1001f5b4f000000000020246cb8584'\n    b'3ac936d55388f2ff288b011add5b1b20cca9cfd19a403ca2c9ecbde09d8734d81b5f2eb1b653caf17491544ddfbc7'\n    b'2f2f4c0c3f22a3362db5ba9d4701000000000000000000000000000000000000000000000000000000000000003d4'\n    b'06b57ffff001f4ff20000000000200044e1258b865d262587c28ff98853bc52bb31266230c1c648cc9004047a5428'\n    b'e285dbf24334585b9a924536a717160ee185a86d1eeb7b19684538685eca761a01000000000000000000000000000'\n    b'000000000000000000000000000000000003d406b5746e1001fce9c010000000020bbf8980e3f7604896821203bf6'\n    b'2f97f311124da1fbb95bf523fcfdb356ad19c9d83cf1408debbd631950b7a95b0c940772119cd8a615a3d44601568'\n    b'713fec80c01000000000000000000000000000000000000000000000000000000000000003e406b573dc6001fec7b'\n    b'0000000000201a650b9b7b9d132e257ff6b336ba7cd96b1796357c4fc8dd7d0bd1ff1de057d547638e54178dbdddf'\n    b'2e81a3b7566860e5264df6066755f9760a893f5caecc5790100000000000000000000000000000000000000000000'\n    b'0000000000000000003e406b5773ae001fcf770000000000206d694b93a2bb5ac23a13ed6749a789ca751cf73d598'\n    b'2c459e0cd9d5d303da74cec91627e0dba856b933983425d7f72958e8f974682632a0fa2acee9cfd81940101000000'\n    b'000000000000000000000000000000000000000000000000000000003e406b578399001f225c010000000020b5780'\n    b'8c188b7315583cf120fe89de923583bc7a8ebff03189145b86bf859b21ba3c4a19948a1263722c45c5601fd10a7ae'\n    b'a7cf73bfa45e060508f109155e80ab010000000000000000000000000000000000000000000000000000000000000'\n    b'03f406b571787001f0816070000000020a6a5b330e816242d54c8586ba9b6d63c19d921171ef3d4525b8ffc635742'\n    b'e83a0fc2da46cf0de0057c1b9fc93d997105ff6cf2c8c43269b446c1dbf5ac18be8c0100000000000000000000000'\n    b'00000000000000000000000000000000000000040406b570ae1761edd8f030000000020b8447f415279dffe8a09af'\n    b'e6f6d5e335a2f6911fce8e1d1866723d5e5e8a53067356a733f87e592ea133328792dd9d676ed83771c8ff0f51992'\n    b'8ce752f159ba6010000000000000000000000000000000000000000000000000000000000000040406b57139d681e'\n    b'd40d000000000020558daee5a4a55fe03d912e35c7b6b0bc19ece82fd5bcb685bc36f2bc381babfd54a598c4356ce'\n    b'620a604004929af14f4c03c42eba017288a4a1d186aedfdd8f4010000000000000000000000000000000000000000'\n    b'000000000000000000000041406b57580f5c1e3e280100000000200381bfc0b2f10c9a3c0fc2dc8ad06388aff8ea5'\n    b'a9f7dba6a945073b021796197364b79f33ff3f3a7ccb676fc0a37b7d831bd5942a05eac314658c6a7e4c4b1a40100'\n    b'00000000000000000000000000000000000000000000000000000000000041406b574303511ec0ae0100000000202'\n    b'aae02063ae0f1025e6acecd5e8e2305956ecaefd185bb47a64ea2ae953233891df3d4c1fc547ab3bbca027c8bbba7'\n    b'44c051add8615d289b567f97c64929dcf201000000000000000000000000000000000000000000000000000000000'\n    b'0000042406b578c4a471e04ee00000000002016603ef45d5a7c02bfbb30f422016746872ff37f8b0b5824a0f70caa'\n    b'668eea5415aad300e70f7d8755d93645d1fd21eda9c40c5d0ed797acd0e07ace34585aaf010000000000000000000'\n    b'000000000000000000000000000000000000000000042406b577bbc3e1ea163000000000020cad8863b312914f2fd'\n    b'2aad6e9420b64859039effd67ac4681a7cf60e42b09b7e7bafa1e8d5131f477785d8338294da0f998844a85b39d24'\n    b'26e839b370e014e3b010000000000000000000000000000000000000000000000000000000000000042406b573935'\n    b'371e20e900000000002053d5e608ce5a12eda5931f86ee81198fdd231fea64cf096e9aeae321cf2efbe241e888d5a'\n    b'af495e4c2a9f11b932db979d7483aeb446f479179b0c0b8d24bfa0e01000000000000000000000000000000000000'\n    b'0000000000000000000000000045406b573c95301e34af0a0000000020df0e494c02ff79e3929bc1f2491077ec4f6'\n    b'a607d7a1a5e1be96536642c98f86e533febd715f8a234028fd52046708551c6b6ac415480a6568aaa35cb94dc7203'\n    b'01000000000000000000000000000000000000000000000000000000000000004f406b57c4c02a1ec54d230000000'\n    b'020341f7d8e7d242e5e46343c40840c44f07e7e7306eb2355521b51502e8070e569485ba7eec4efdff0fc755af6e7'\n    b'3e38b381a88b0925a68193a25da19d0f616e9f0100000000000000000000000000000000000000000000000000000'\n    b'00000000050406b575be8251e1f61010000000020cd399f8078166ca5f0bdd1080ab1bb22d3c271b9729b6000b44f'\n    b'4592cc9fab08c00ebab1e7cd88677e3b77c1598c7ac58660567f49f3a30ec46a48a1ae7652fe01000000000000000'\n    b'0000000000000000000000000000000000000000000000052406b57d55b211e6f53090000000020c6c14ed4a53bbb'\n    b'4f181acf2bbfd8b74d13826732f2114140ca99ca371f7dd87c51d18a05a1a6ffa37c041877fa33c2229a45a0ab66b'\n    b'5530f914200a8d6639a6f010000000000000000000000000000000000000000000000000000000000000055406b57'\n    b'0d5b1d1eff1c0900'\n)\n"
  },
  {
    "path": "tests/unit/wallet/test_ledger.py",
    "content": "import os\nfrom unittest import TestCase\nfrom binascii import hexlify\n\nfrom lbry.testcase import AsyncioTestCase\nfrom lbry.wallet import Wallet, Account, Transaction, Output, Input, Ledger, Database, Headers\n\nfrom tests.unit.wallet.test_transaction import get_transaction, get_output\nfrom tests.unit.wallet.test_headers import HEADERS, block_bytes\n\n\nclass MockNetwork:\n\n    def __init__(self, history, transaction):\n        self.history = history\n        self.transaction = transaction\n        self.address = None\n        self.get_history_called = []\n        self.get_transaction_called = []\n        self.is_connected = False\n\n    def retriable_call(self, function, *args, **kwargs):\n        return function(*args, **kwargs)\n\n    async def get_history(self, address):\n        self.get_history_called.append(address)\n        self.address = address\n        return self.history\n\n    async def get_merkle(self, txid, height):\n        return {'merkle': ['abcd01'], 'pos': 1}\n\n    async def get_transaction(self, tx_hash, _=None):\n        self.get_transaction_called.append(tx_hash)\n        return self.transaction[tx_hash]\n\n    async def get_transaction_and_merkle(self, tx_hash, known_height=None):\n        tx = await self.get_transaction(tx_hash)\n        merkle = {'block_height': -1}\n        if known_height:\n            merkle = await self.get_merkle(tx_hash, known_height)\n        return tx, merkle\n\n    async def get_transaction_batch(self, txids, restricted):\n        return {\n            txid: await self.get_transaction_and_merkle(txid)\n            for txid in txids\n        }\n\n\nclass LedgerTestCase(AsyncioTestCase):\n\n    async def asyncSetUp(self):\n        self.ledger = Ledger({\n            'db': Database(':memory:'),\n            'headers': Headers(':memory:')\n        })\n        self.ledger.headers.checkpoints = {}\n        await self.ledger.headers.open()\n        self.account = Account.generate(self.ledger, Wallet(), \"lbryum\")\n        await self.ledger.db.open()\n\n    async def asyncTearDown(self):\n        await self.ledger.db.close()\n\n    def make_header(self, **kwargs):\n        header = {\n            'bits': 486604799,\n            'block_height': 0,\n            'merkle_root': b'4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b',\n            'nonce': 2083236893,\n            'prev_block_hash': b'0000000000000000000000000000000000000000000000000000000000000000',\n            'timestamp': 1231006505,\n            'claim_trie_root': b'4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b',\n            'version': 1\n        }\n        header.update(kwargs)\n        header['merkle_root'] = header['merkle_root'].ljust(64, b'a')\n        header['prev_block_hash'] = header['prev_block_hash'].ljust(64, b'0')\n        return self.ledger.headers.serialize(header)\n\n    def add_header(self, **kwargs):\n        serialized = self.make_header(**kwargs)\n        self.ledger.headers.io.seek(0, os.SEEK_END)\n        self.ledger.headers.io.write(serialized)\n        self.ledger.headers._size = self.ledger.headers.io.seek(0, os.SEEK_END) // self.ledger.headers.header_size\n\n\nclass TestUtils(TestCase):\n\n    def test_valid_address(self):\n        self.assertTrue(Ledger.is_script_address(\"rCz6yb1p33oYHToGZDzTjX7nFKaU3kNgBd\"))\n\n\nclass TestSynchronization(LedgerTestCase):\n\n    async def test_update_history(self):\n        txid1 = '252bda9b22cc902ca2aa2de3548ee8baf06b8501ff7bfb3b0b7d980dbd1bf792'\n        txid2 = 'ab9c0654dd484ac20437030f2034e25dcb29fc507e84b91138f80adc3af738f9'\n        txid3 = 'a2ae3d1db3c727e7d696122cab39ee20a7f81856dab7019056dd539f38c548a0'\n        txid4 = '047cf1d53ef68f0fd586d46f90c09ff8e57a4180f67e7f4b8dd0135c3741e828'\n\n        account = Account.generate(self.ledger, Wallet(), \"torba\")\n        address = await account.receiving.get_or_create_usable_address()\n        address_details = await self.ledger.db.get_address(address=address)\n        self.assertIsNone(address_details['history'])\n\n        self.add_header(block_height=0, merkle_root=b'abcd04')\n        self.add_header(block_height=1, merkle_root=b'abcd04')\n        self.add_header(block_height=2, merkle_root=b'abcd04')\n        self.add_header(block_height=3, merkle_root=b'abcd04')\n        self.ledger.network = MockNetwork([\n            {'tx_hash': txid1, 'height': 0},\n            {'tx_hash': txid2, 'height': 1},\n            {'tx_hash': txid3, 'height': 2},\n        ], {\n            txid1: hexlify(get_transaction(get_output(1)).raw),\n            txid2: hexlify(get_transaction(get_output(2)).raw),\n            txid3: hexlify(get_transaction(get_output(3)).raw),\n        })\n        await self.ledger.update_history(address, '')\n        self.assertListEqual(self.ledger.network.get_history_called, [address])\n        self.assertListEqual(self.ledger.network.get_transaction_called, [txid1, txid2, txid3])\n\n        address_details = await self.ledger.db.get_address(address=address)\n\n        self.assertEqual(\n            address_details['history'],\n            f'{txid1}:0:'\n            f'{txid2}:1:'\n            f'{txid3}:2:'\n        )\n\n        self.ledger.network.get_history_called = []\n        self.ledger.network.get_transaction_called = []\n        self.assertEqual(0, len(self.ledger._tx_cache))\n        await self.ledger.update_history(address, '')\n        self.assertListEqual(self.ledger.network.get_history_called, [address])\n        self.assertListEqual(self.ledger.network.get_transaction_called, [])\n\n        self.ledger.network.history.append({'tx_hash': txid4, 'height': 3})\n        self.ledger.network.transaction[txid4] = hexlify(get_transaction(get_output(4)).raw)\n        self.ledger.network.get_history_called = []\n        self.ledger.network.get_transaction_called = []\n        await self.ledger.update_history(address, '')\n        self.assertListEqual(self.ledger.network.get_history_called, [address])\n        self.assertListEqual(self.ledger.network.get_transaction_called, [txid4])\n        address_details = await self.ledger.db.get_address(address=address)\n        self.assertEqual(\n            address_details['history'],\n            f'{txid1}:0:'\n            f'{txid2}:1:'\n            f'{txid3}:2:'\n            f'{txid4}:3:'\n        )\n\n\nclass MocHeaderNetwork(MockNetwork):\n    def __init__(self, responses):\n        super().__init__(None, None)\n        self.responses = responses\n\n    async def get_headers(self, height, blocks):\n        return self.responses[height]\n\n\nclass BlockchainReorganizationTests(LedgerTestCase):\n\n    async def test_1_block_reorganization(self):\n        self.ledger.network = MocHeaderNetwork({\n            10: {'height': 10, 'count': 5, 'hex': hexlify(\n                HEADERS[block_bytes(10):block_bytes(15)]\n            )},\n            15: {'height': 15, 'count': 0, 'hex': b''}\n        })\n        headers = self.ledger.headers\n        await headers.connect(0, HEADERS[:block_bytes(10)])\n        self.add_header(block_height=len(headers))\n        self.assertEqual(10, headers.height)\n        await self.ledger.receive_header([{\n            'height': 11, 'hex': hexlify(self.make_header(block_height=11))\n        }])\n\n    async def test_3_block_reorganization(self):\n        self.ledger.network = MocHeaderNetwork({\n            10: {'height': 10, 'count': 5, 'hex': hexlify(\n                HEADERS[block_bytes(10):block_bytes(15)]\n            )},\n            11: {'height': 11, 'count': 1, 'hex': hexlify(self.make_header(block_height=11))},\n            12: {'height': 12, 'count': 1, 'hex': hexlify(self.make_header(block_height=12))},\n            15: {'height': 15, 'count': 0, 'hex': b''}\n        })\n        headers = self.ledger.headers\n        await headers.connect(0, HEADERS[:block_bytes(10)])\n        self.add_header(block_height=len(headers))\n        self.add_header(block_height=len(headers))\n        self.add_header(block_height=len(headers))\n        self.assertEqual(headers.height, 12)\n        await self.ledger.receive_header([{\n            'height': 13, 'hex': hexlify(self.make_header(block_height=13))\n        }])\n\n\nclass BasicAccountingTests(LedgerTestCase):\n\n    async def test_empty_state(self):\n        self.assertEqual(await self.account.get_balance(), 0)\n\n    async def test_balance(self):\n        address = await self.account.receiving.get_or_create_usable_address()\n        hash160 = self.ledger.address_to_hash160(address)\n\n        tx = Transaction(is_verified=True)\\\n            .add_outputs([Output.pay_pubkey_hash(100, hash160)])\n        await self.ledger.db.insert_transaction(tx)\n        await self.ledger.db.save_transaction_io(\n            tx, address, hash160, f'{tx.id}:1:'\n        )\n        self.assertEqual(await self.account.get_balance(), 100)\n\n        tx = Transaction(is_verified=True)\\\n            .add_outputs([Output.pay_claim_name_pubkey_hash(100, 'foo', b'', hash160)])\n        await self.ledger.db.insert_transaction(tx)\n        await self.ledger.db.save_transaction_io(\n            tx, address, hash160, f'{tx.id}:1:'\n        )\n        self.assertEqual(await self.account.get_balance(), 100)  # claim names don't count towards balance\n        self.assertEqual(await self.account.get_balance(include_claims=True), 200)\n\n    async def test_get_utxo(self):\n        address = yield self.account.receiving.get_or_create_usable_address()\n        hash160 = self.ledger.address_to_hash160(address)\n\n        tx = Transaction(is_verified=True)\\\n            .add_outputs([Output.pay_pubkey_hash(100, hash160)])\n        await self.ledger.db.save_transaction_io(\n            'insert', tx, address, hash160, f'{tx.id}:1:'\n        )\n\n        utxos = await self.account.get_utxos()\n        self.assertEqual(len(utxos), 1)\n\n        tx = Transaction(is_verified=True)\\\n            .add_inputs([Input.spend(utxos[0])])\n        await self.ledger.db.save_transaction_io(\n            'insert', tx, address, hash160, f'{tx.id}:1:'\n        )\n        self.assertEqual(await self.account.get_balance(include_claims=True), 0)\n\n        utxos = await self.account.get_utxos()\n        self.assertEqual(len(utxos), 0)\n"
  },
  {
    "path": "tests/unit/wallet/test_mnemonic.py",
    "content": "import unittest\nfrom binascii import hexlify\n\nfrom lbry.wallet.mnemonic import Mnemonic\n\n\nclass TestMnemonic(unittest.TestCase):\n\n    def test_mnemonic_to_seed(self):\n        seed = Mnemonic.mnemonic_to_seed(mnemonic='foobar', passphrase='torba')\n        self.assertEqual(\n            hexlify(seed),\n            b'475a419db4e991cab14f08bde2d357e52b3e7241f72c6d8a2f92782367feeee9f403dc6a37c26a3f02ab9'\n            b'dec7f5063161eb139cea00da64cd77fba2f07c49ddc'\n        )\n\n    def test_make_seed_decode_encode(self):\n        iters = 10\n        m = Mnemonic('en')\n        for _ in range(iters):\n            seed = m.make_seed()\n            i = m.mnemonic_decode(seed)\n            self.assertEqual(m.mnemonic_encode(i), seed)\n"
  },
  {
    "path": "tests/unit/wallet/test_schema_signing.py",
    "content": "from binascii import unhexlify\n\nfrom lbry.testcase import AsyncioTestCase\nfrom lbry.wallet.constants import CENT, NULL_HASH32\nfrom lbry.wallet.bip32 import PrivateKey, KeyPath\nfrom lbry.wallet.mnemonic import Mnemonic\nfrom lbry.wallet import Ledger, Database, Headers, Transaction, Input, Output\nfrom lbry.schema.claim import Claim\nfrom lbry.crypto.hash import sha256\n\n\ndef get_output(amount=CENT, pubkey_hash=NULL_HASH32):\n    return Transaction() \\\n        .add_outputs([Output.pay_pubkey_hash(amount, pubkey_hash)]) \\\n        .outputs[0]\n\n\ndef get_input():\n    return Input.spend(get_output())\n\n\ndef get_tx():\n    return Transaction().add_inputs([get_input()])\n\n\nasync def get_channel(claim_name='@foo'):\n    seed = Mnemonic.mnemonic_to_seed(Mnemonic().make_seed(), '')\n    key = PrivateKey.from_seed(Ledger, seed)\n    channel_key = key.child(KeyPath.CHANNEL).child(0)\n    channel_txo = Output.pay_claim_name_pubkey_hash(CENT, claim_name, Claim(), b'abc')\n    channel_txo.set_channel_private_key(channel_key)\n    get_tx().add_outputs([channel_txo])\n    return channel_txo\n\n\ndef get_stream(claim_name='foo'):\n    stream_txo = Output.pay_claim_name_pubkey_hash(CENT, claim_name, Claim(), b'abc')\n    get_tx().add_outputs([stream_txo])\n    return stream_txo\n\n\nclass TestSigningAndValidatingClaim(AsyncioTestCase):\n\n    async def test_successful_create_sign_and_validate(self):\n        channel = await get_channel()\n        stream = get_stream()\n        stream.sign(channel)\n        self.assertTrue(stream.is_signed_by(channel))\n\n    async def test_fail_to_validate_on_wrong_channel(self):\n        stream = get_stream()\n        stream.sign(await get_channel())\n        self.assertFalse(stream.is_signed_by(await get_channel()))\n\n    async def test_fail_to_validate_altered_claim(self):\n        channel = await get_channel()\n        stream = get_stream()\n        stream.sign(channel)\n        self.assertTrue(stream.is_signed_by(channel))\n        stream.claim.stream.title = 'hello'\n        self.assertFalse(stream.is_signed_by(channel))\n\n    async def test_valid_private_key_for_cert(self):\n        channel = await get_channel()\n        self.assertTrue(channel.is_channel_private_key(channel.private_key))\n\n    async def test_fail_to_load_wrong_private_key_for_cert(self):\n        channel = await get_channel()\n        self.assertFalse(channel.is_channel_private_key((await get_channel()).private_key))\n\n\nclass TestValidatingOldSignatures(AsyncioTestCase):\n\n    def test_signed_claim_made_by_ytsync(self):\n        stream_tx = Transaction(unhexlify(\n            b'0100000001eb2a756e15bde95db3d2ae4a6e9b2796a699087890644607b5b04a5f15b67062010000006a4'\n            b'7304402206444b920bd318a07d9b982e30eb66245fdaaa6c9866e1f6e5900161d9b0ffd70022036464714'\n            b'4f1830898a2042aa0d6cef95a243799cc6e36630a58d411e2f9111f00121029b15f9a00a7c3f21b10bd4b'\n            b'98ab23a9e895bd9160e21f71317862bf55fbbc89effffffff0240420f0000000000fd1503b52268657265'\n            b'2d6172652d352d726561736f6e732d692d6e657874636c6f75642d746c674dd302080110011aee0408011'\n            b'2a604080410011a2b4865726520617265203520526561736f6e73204920e29da4efb88f204e657874636c'\n            b'6f7564207c20544c4722920346696e64206f7574206d6f72652061626f7574204e657874636c6f75643a2'\n            b'068747470733a2f2f6e657874636c6f75642e636f6d2f0a0a596f752063616e2066696e64206d65206f6e'\n            b'20746865736520736f6369616c733a0a202a20466f72756d733a2068747470733a2f2f666f72756d2e686'\n            b'5617679656c656d656e742e696f2f0a202a20506f64636173743a2068747470733a2f2f6f6666746f7069'\n            b'63616c2e6e65740a202a2050617472656f6e3a2068747470733a2f2f70617472656f6e2e636f6d2f74686'\n            b'56c696e757867616d65720a202a204d657263683a2068747470733a2f2f746565737072696e672e636f6d'\n            b'2f73746f7265732f6f6666696369616c2d6c696e75782d67616d65720a202a205477697463683a2068747'\n            b'470733a2f2f7477697463682e74762f786f6e64616b0a202a20547769747465723a2068747470733a2f2f'\n            b'747769747465722e636f6d2f7468656c696e757867616d65720a0a2e2e2e0a68747470733a2f2f7777772'\n            b'e796f75747562652e636f6d2f77617463683f763d4672546442434f535f66632a0f546865204c696e7578'\n            b'2047616d6572321c436f7079726967687465642028636f6e7461637420617574686f722938004a2968747'\n            b'470733a2f2f6265726b2e6e696e6a612f7468756d626e61696c732f4672546442434f535f666352005a00'\n            b'1a41080110011a30040e8ac6e89c061f982528c23ad33829fd7146435bf7a4cc22f0bff70c4fe0b91fd36'\n            b'da9a375e3e1c171db825bf5d1f32209766964656f2f6d70342a5c080110031a4062b2dd4c45e364030fbf'\n            b'ad1a6fefff695ebf20ea33a5381b947753e2a0ca359989a5cc7d15e5392a0d354c0b68498382b2701b22c'\n            b'03beb8dcb91089031b871e72214feb61536c007cdf4faeeaab4876cb397feaf6b516d7576a914f4f43f6f'\n            b'7a472bbf27fa3630329f771135fc445788ac86ff0600000000001976a914cef0fe3eeaf04416f0c3ff3e7'\n            b'8a598a081e70ee788ac00000000'\n        ))\n        stream = stream_tx.outputs[0]\n\n        channel_tx = Transaction(unhexlify(\n            b'010000000192a1e1e3f66b8ca05a021cfa5fb6645ebc066b46639ccc9b3781fa588a88da65010000006a4'\n            b'7304402206be09a355f6abea8a10b5512180cd258460b42d516b5149431ffa3230a02533a0220325e83c6'\n            b'176b295d633b18aad67adb4ad766d13152536ac04583f86d14645c9901210269c63bc8bac8143ef02f972'\n            b'4a4ab35b12bdfa65ee1ad8c0db3d6511407a4cc2effffffff0240420f000000000091b50e405468654c69'\n            b'6e757847616d65724c6408011002225e0801100322583056301006072a8648ce3d020106052b8104000a0'\n            b'34200043878b1edd4a1373149909ef03f4339f6da9c2bd2214c040fd2e530463ffe66098eca14fc70b50f'\n            b'f3aefd106049a815f595ed5a13eda7419ad78d9ed7ae473f176d7576a914994dad5f21c384ff526749b87'\n            b'6d9d017d257b69888ac00dd6d00000000001976a914979202508a44f0e8290cea80787c76f98728845388'\n            b'ac00000000'\n        ))\n        channel = channel_tx.outputs[0]\n\n        ledger = Ledger({\n            'db': Database(':memory:'),\n            'headers': Headers(':memory:')\n        })\n\n        self.assertTrue(stream.is_signed_by(channel, ledger))\n\n    def test_another_signed_claim_made_by_ytsync(self):\n        stream_tx = Transaction(unhexlify(\n            b'010000000185870fabdd6bd2d57749afebc0b239e8d0ebeb6f3647d6cfcabd5ea2200ac632010000006b4'\n            b'83045022100877c86de154e39f21959bc2157865071924adb7930a7a8910714f27398cd2689022074270f'\n            b'074ae260fff319d5e0c030691821bc75b82ff0179898ac3eaeda4123eb01210200328f7f001f22ea25d72'\n            b'ba37379e3065020c4d8371d9199dc4e3770084e26b9ffffffff0240420f0000000000fdcc05b527746865'\n            b'2d637269746963616c2d6e6565642d666f722d696e646570656e64656e742d6d656469614d85050191bba'\n            b'd064bdc455b9ebddeeb559686b13f027615384ec7c9d981c3c21a6e3d723a654e86bd707d21174c4f697f'\n            b'5080cf367a3b2dfc059e6cc14a962631df69b9886f4d8b97cb339b14633966fd5ac7d75edacdf30ac5010'\n            b'a90010a304af34d1c1467ebfc8785e2a49c7d5bec3cc6db94db858f1dcf95e4256564fba586d6e01f496d'\n            b'f2a34344e021d2725ffd12197468652d637269746963616c2d6e6565642d666f722e6d703418ee97eac10'\n            b'22209766964656f2f6d70343230ba13e6b667a9acef7e1b1caa88b9eb1d4680dea84b1d3e838266595805'\n            b'ab3343855c20af35012f942ce0d5111ce080331a1f436f7079726967687465642028636f6e74616374207'\n            b'075626c69736865722928e2e3c98d065a0908800f10b80818f314423954686520437269746963616c204e'\n            b'65656420666f7220496e646570656e64656e74204d65646961207c20476c656e6e20477265656e77616c6'\n            b'44af006496e636c7564657320616e20696e74726f64756374696f6e20627920546f6d20576f6f64732e20'\n            b'5265636f7264656420696e204c616b65204a61636b736f6e2c2054657861732c206f6e20446563656d626'\n            b'57220342c20323032312e0a0a526f6e205061756c27732074776f2063616d706169676e7320666f722070'\n            b'7265736964656e7420283230303820616e64203230313229207765726520776174657273686564206d6f6'\n            b'd656e747320666f72206c6962657274792d6d696e6465642070656f706c652061726f756e642074686520'\n            b'776f726c642e205468652022526f6e205061756c205265766f6c7574696f6e22e2809463656e746572656'\n            b'42061726f756e642068697320756e64696c75746564206d657373616765206f662070656163652c207072'\n            b'6f70657274792c20616e64206d61726b657473e280946368616e6765642074686520776179206d696c6c6'\n            b'96f6e732074686f756768742061626f75742074686520416d65726963616e20656d7069726520616e6420'\n            b'74686520416d65726963616e2066696e616e6369616c2073797374656d2e2044722e205061756c2773206'\n            b'66f637573206f6e2063656e7472616c2062616e6b696e6720616e6420666f726569676e20706f6c696379'\n            b'2063617567687420706f6c6974696369616e7320616e642070756e64697473206f66662067756172642c2'\n            b'0666f7263696e67207468656d20746f20736372616d626c6520666f72206578706c616e6174696f6e7320'\n            b'6f66206f7572204d6964646c65204561737420706f6c69637920616e6420536f766965742d7374796c652'\n            b'063656e7472616c20706c616e6e696e6720617420746865204665642e20506f6c697469637320696e2041'\n            b'6d657269636120686173206e6f74206265656e207468652073616d652073696e636520746865202247697'\n            b'56c69616e69206d6f6d656e742220616e642022456e6420746865204665642e222054686520526f6e2050'\n            b'61756c205265766f6c7574696f6e2077617320626f7468206120706f6c69746963616c20616e642063756'\n            b'c747572616c207068656e6f6d656e6f6e2e0a0a303a303020496e74726f64756374696f6e20627920546f'\n            b'6d20576f6f64730a343a323720476c656e6e20477265656e77616c640a2e2e2e0a68747470733a2f2f777'\n            b'7772e796f75747562652e636f6d2f77617463683f763d4e4b70706d52467673453052292a276874747073'\n            b'3a2f2f7468756d626e61696c732e6c6272792e636f6d2f4e4b70706d5246767345305a046e6577735a096'\n            b'3617468656472616c5a0f636f72706f72617465206d656469615a08637269746963616c5a0f676c656e6e'\n            b'20677265656e77616c645a0b696e646570656e64656e745a0a6a6f75726e616c69736d5a056d656469615'\n            b'a056d697365735a08706f6c69746963735a0a70726f706167616e64615a08726f6e207061756c5a057472'\n            b'757468620208016d7576a9140969964db5b5744e2d2d0de797f5904efc80d02188acc8814200000000001'\n            b'976a91439086597f9cfc066f4749b8bb245bf561714fda888ac00000000'\n        ))\n        stream = stream_tx.outputs[0]\n\n        channel_tx = Transaction(unhexlify(\n            b'01000000011d47b91b409b317e427adb87ec4b0bfc9fad2abf6ec3296f41918e4b3cb9d4e7010000006a4'\n            b'7304402205e53ef7fc643ed00f0240dd1c3302b82141f481ed071cbcdd6b6ec6166ffd4e002203eb28ce6'\n            b'39f80253f66ff3bf45288a60133d7f5625217d1ecf3b57da440b559f012103b852d61074eb995b702a800'\n            b'f284e937ece4fea7f023beb70e6b0d1bff36d64b9ffffffff0240420f0000000000fdde01b506406d6973'\n            b'65734db801001299010a583056301006072a8648ce3d020106052b8104000a034200047ddb1d639d7bdd0'\n            b'953d9ab0bf9e971a632f85f9823c1d85780aa3e0a702b503c2962d00f67360e803514bf5864710925aacb'\n            b'effd9597532c7e60eb21b4e3fd03223d2a3b68747470733a2f2f7468756d626e61696c732e6c6272792e6'\n            b'36f6d2f62616e6e65722d55436d54362d43684b7061694956753266684549734e7451420a6d697365736d'\n            b'656469614ad401466561747572656420766964656f732066726f6d20746865204d6973657320496e73746'\n            b'9747574652e20546865204d6973657320496e737469747574652070726f6d6f7465732041757374726961'\n            b'6e2065636f6e6f6d6963732c2066726565646f6d2c20616e6420706561636520696e20746865206c69626'\n            b'572616c20696e74656c6c65637475616c20747261646974696f6e206f66204c756477696720766f6e204d'\n            b'69736573207468726f7567682072657365617263682c207075626c697368696e672c20616e64206564756'\n            b'36174696f6e2e52362a3468747470733a2f2f7468756d626e61696c732e6c6272792e636f6d2f55436d54'\n            b'362d43684b7061694956753266684549734e74516d7576a914cd77ded2400e6569f03a2580244bb395f95'\n            b'f91fc88ac344ab701000000001976a914cabdbfce726d2fda92ffe0041a4303f6c6c34cda88ac00000000'\n        ))\n        channel = channel_tx.outputs[0]\n\n        ledger = Ledger({\n            'db': Database(':memory:'),\n            'headers': Headers(':memory:')\n        })\n\n        self.assertTrue(stream.is_signed_by(channel, ledger))\n\n    def test_claim_signed_using_ecdsa_validates_with_coincurve(self):\n        channel_tx = Transaction(unhexlify(\n            \"0100000001b91d829283c0d80cb8113d5f36b6da3dfe9df3e783f158bfb3fd1b2b178d7fc9010000006b48\"\n            \"3045022100f4e2b4ee38388c3d3a62f4b12fdd413f6f140168e85884bbeb33a3f2d3159ef502201721200f\"\n            \"4a4f3b87484d4f47c9054e31cd3ba451dd3886a7f9f854893e7c8cf90121023f9e906e0c120f3bf74feb40\"\n            \"f01ddeafbeb1856d91938c3bef25bed06767247cffffffff0200e1f5050000000081b505406368616e4c5d\"\n            \"00125a0a583056301006072a8648ce3d020106052b8104000a03420004d7fa13fd8e57f3a0b878eaaf3d17\"\n            \"9144d25ddbe4a3e4440a661f51b4134c6a13c9c98678ff8411932e60fd97d7baf03ea67ebcc21097230cfb\"\n            \"2241348aadb55e6d7576a9149c6d700f89c77f0e8c650ba05656f8f2392782d388acf47c95350000000019\"\n            \"76a914d9502233e0e1fc76e13e36c546f704c3124d5eaa88ac00000000\"\n        ))\n        channel = channel_tx.outputs[0]\n\n        stream_tx = Transaction(unhexlify(\n            \"010000000116a1d90763f2e3a2348c7fb438a23f232b15e3ffe3f058c3b2ab52c8bed8dcb5010000006b48\"\n            \"30450221008f38561b3a16944c63b4f4f1562f1efe1b2060f31d249e234003ee5e3461756f02205773c99e\"\n            \"83c968728e4f2433a13871c6ad23f6c10368ac52fa62a09f3f7ef5fd012102597f39845b98e2415b777aa0\"\n            \"3849d346d287af7970deb05f11214b3418ae9d82ffffffff0200e1f50500000000fd0c01b505636c61696d\"\n            \"4ce8012e6e40fa5fee1b915af3b55131dcbcebee34ab9148292b084ce3741f2e0db49783f3d854ac885f2b\"\n            \"6304a76ef7048046e338dd414ba4c64e8468651768ffaaf550c8560637ac8c477ea481ac2a9264097240f4\"\n            \"ab0a90010a8d010a3056bf5dbae43f77a63d075b0f2ae9c7c3e3098db93779c7f9840da0f4db9c2f8c8454\"\n            \"f4edd1373e2b64ee2e68350d916e120b746d706c69647879363171180322186170706c69636174696f6e2f\"\n            \"6f637465742d73747265616d3230f293f5acf4310562d4a41f6620167fe6d83761a98d36738908ce5c8776\"\n            \"1642710e55352a396276a42eda92ff5856f46f6d7576a91434bd3dc4c45cc0635eb2ad5da658727e5442ca\"\n            \"0f88ace82f902f000000001976a91427b27c89eaebf68d063c107241584c07e5a6ccc688ac00000000\"\n        ))\n        stream = stream_tx.outputs[0]\n\n        ledger = Ledger({'db': Database(':memory:'), 'headers': Headers(':memory:')})\n        self.assertTrue(stream.is_signed_by(channel, ledger))\n\n\nclass TestValidateSignContent(AsyncioTestCase):\n\n    async def test_sign_some_content(self):\n        some_content = \"MEANINGLESS CONTENT AEE3353320\".encode()\n        timestamp_str = \"1630564175\"\n        channel = await get_channel()\n        signature = channel.sign_data(some_content, timestamp_str)\n        pieces = [timestamp_str.encode(), channel.claim_hash, some_content]\n        self.assertTrue(Output.is_signature_valid(\n            unhexlify(signature.encode()),\n            sha256(b''.join(pieces)),\n            channel.claim.channel.public_key_bytes\n        ))\n"
  },
  {
    "path": "tests/unit/wallet/test_script.py",
    "content": "import unittest\nfrom binascii import hexlify, unhexlify\n\nfrom lbry.wallet.bcd_data_stream import BCDataStream\nfrom lbry.wallet.script import (\n    InputScript, OutputScript, Template, ParseError, tokenize, push_data,\n    PUSH_SINGLE, PUSH_INTEGER, PUSH_MANY, OP_HASH160, OP_EQUAL\n)\n\n\ndef parse(opcodes, source):\n    template = Template('test', opcodes)\n    s = BCDataStream()\n    for t in source:\n        if isinstance(t, bytes):\n            s.write_many(push_data(t))\n        elif isinstance(t, int):\n            s.write_uint8(t)\n        else:\n            raise ValueError()\n    s.reset()\n    return template.parse(tokenize(s))\n\n\nclass TestScriptTemplates(unittest.TestCase):\n\n    def test_push_data(self):\n        self.assertDictEqual(parse(\n            (PUSH_SINGLE('script_hash'),),\n            (b'abcdef',)\n        ), {\n            'script_hash': b'abcdef'\n        }\n        )\n        self.assertDictEqual(parse(\n            (PUSH_SINGLE('first'), PUSH_INTEGER('rating')),\n            (b'Satoshi', (1000).to_bytes(2, 'little'))\n        ), {\n            'first': b'Satoshi',\n            'rating': 1000,\n        }\n        )\n        self.assertDictEqual(parse(\n            (OP_HASH160, PUSH_SINGLE('script_hash'), OP_EQUAL),\n            (OP_HASH160, b'abcdef', OP_EQUAL)\n        ), {\n            'script_hash': b'abcdef'\n        }\n        )\n\n    def test_push_data_many(self):\n        self.assertDictEqual(parse(\n            (PUSH_MANY('names'),),\n            (b'amit',)\n        ), {\n            'names': [b'amit']\n        }\n        )\n        self.assertDictEqual(parse(\n            (PUSH_MANY('names'),),\n            (b'jeremy', b'amit', b'victor')\n        ), {\n            'names': [b'jeremy', b'amit', b'victor']\n        }\n        )\n        self.assertDictEqual(parse(\n            (OP_HASH160, PUSH_MANY('names'), OP_EQUAL),\n            (OP_HASH160, b'grin', b'jack', OP_EQUAL)\n        ), {\n            'names': [b'grin', b'jack']\n        }\n        )\n\n    def test_push_data_mixed(self):\n        self.assertDictEqual(parse(\n            (PUSH_SINGLE('CEO'), PUSH_MANY('Devs'), PUSH_SINGLE('CTO'), PUSH_SINGLE('State')),\n            (b'jeremy', b'lex', b'amit', b'victor', b'jack', b'grin', b'NH')\n        ), {\n            'CEO': b'jeremy',\n            'CTO': b'grin',\n            'Devs': [b'lex', b'amit', b'victor', b'jack'],\n            'State': b'NH'\n        }\n        )\n\n    def test_push_data_many_separated(self):\n        self.assertDictEqual(parse(\n            (PUSH_MANY('Chiefs'), OP_HASH160, PUSH_MANY('Devs')),\n            (b'jeremy', b'grin', OP_HASH160, b'lex', b'jack')\n        ), {\n            'Chiefs': [b'jeremy', b'grin'],\n            'Devs': [b'lex', b'jack']\n        }\n        )\n\n    def test_push_data_many_not_separated(self):\n        with self.assertRaisesRegex(ParseError, 'consecutive PUSH_MANY'):\n            parse((PUSH_MANY('Chiefs'), PUSH_MANY('Devs')), (b'jeremy', b'grin', b'lex', b'jack'))\n\n\nclass TestRedeemPubKeyHash(unittest.TestCase):\n\n    def redeem_pubkey_hash(self, sig, pubkey):\n        # this checks that factory function correctly sets up the script\n        src1 = InputScript.redeem_pubkey_hash(unhexlify(sig), unhexlify(pubkey))\n        self.assertEqual(src1.template.name, 'pubkey_hash')\n        self.assertEqual(hexlify(src1.values['signature']), sig)\n        self.assertEqual(hexlify(src1.values['pubkey']), pubkey)\n        # now we test that it will round trip\n        src2 = InputScript(src1.source)\n        self.assertEqual(src2.template.name, 'pubkey_hash')\n        self.assertEqual(hexlify(src2.values['signature']), sig)\n        self.assertEqual(hexlify(src2.values['pubkey']), pubkey)\n        return hexlify(src1.source)\n\n    def test_redeem_pubkey_hash_1(self):\n        self.assertEqual(\n            self.redeem_pubkey_hash(\n                b'30450221009dc93f25184a8d483745cd3eceff49727a317c9bfd8be8d3d04517e9cdaf8dd502200e'\n                b'02dc5939cad9562d2b1f303f185957581c4851c98d497af281118825e18a8301',\n                b'025415a06514230521bff3aaface31f6db9d9bbc39bf1ca60a189e78731cfd4e1b'\n            ),\n            b'4830450221009dc93f25184a8d483745cd3eceff49727a317c9bfd8be8d3d04517e9cdaf8dd502200e02d'\n            b'c5939cad9562d2b1f303f185957581c4851c98d497af281118825e18a830121025415a06514230521bff3'\n            b'aaface31f6db9d9bbc39bf1ca60a189e78731cfd4e1b'\n        )\n\n\nclass TestRedeemScriptHash(unittest.TestCase):\n\n    def redeem_script_hash(self, sigs, pubkeys):\n        # this checks that factory function correctly sets up the script\n        src1 = InputScript.redeem_multi_sig_script_hash(\n            [unhexlify(sig) for sig in sigs],\n            [unhexlify(pubkey) for pubkey in pubkeys]\n        )\n        subscript1 = src1.values['script']\n        self.assertEqual(src1.template.name, 'script_hash+multi_sig')\n        self.assertListEqual([hexlify(v) for v in src1.values['signatures']], sigs)\n        self.assertListEqual([hexlify(p) for p in subscript1.values['pubkeys']], pubkeys)\n        self.assertEqual(subscript1.values['signatures_count'], len(sigs))\n        self.assertEqual(subscript1.values['pubkeys_count'], len(pubkeys))\n        # now we test that it will round trip\n        src2 = InputScript(src1.source)\n        subscript2 = src2.values['script']\n        self.assertEqual(src2.template.name, 'script_hash+multi_sig')\n        self.assertListEqual([hexlify(v) for v in src2.values['signatures']], sigs)\n        self.assertListEqual([hexlify(p) for p in subscript2.values['pubkeys']], pubkeys)\n        self.assertEqual(subscript2.values['signatures_count'], len(sigs))\n        self.assertEqual(subscript2.values['pubkeys_count'], len(pubkeys))\n        return hexlify(src1.source)\n\n    def test_redeem_script_hash_1(self):\n        self.assertEqual(\n            self.redeem_script_hash([\n                b'3045022100fec82ed82687874f2a29cbdc8334e114af645c45298e85bb1efe69fcf15c617a0220575'\n                b'e40399f9ada388d8e522899f4ec3b7256896dd9b02742f6567d960b613f0401',\n                b'3044022024890462f731bd1a42a4716797bad94761fc4112e359117e591c07b8520ea33b02201ac68'\n                b'9e35c4648e6beff1d42490207ba14027a638a62663b2ee40153299141eb01',\n                b'30450221009910823e0142967a73c2d16c1560054d71c0625a385904ba2f1f53e0bc1daa8d02205cd'\n                b'70a89c6cf031a8b07d1d5eb0d65d108c4d49c2d403f84fb03ad3dc318777a01'\n            ], [\n                b'0372ba1fd35e5f1b1437cba0c4ebfc4025b7349366f9f9c7c8c4b03a47bd3f68a4',\n                b'03061d250182b2db1ba144167fd8b0ef3fe0fc3a2fa046958f835ffaf0dfdb7692',\n                b'02463bfbc1eaec74b5c21c09239ae18dbf6fc07833917df10d0b43e322810cee0c',\n                b'02fa6a6455c26fb516cfa85ea8de81dd623a893ffd579ee2a00deb6cdf3633d6bb',\n                b'0382910eae483ce4213d79d107bfc78f3d77e2a31ea597be45256171ad0abeaa89'\n            ]),\n            b'00483045022100fec82ed82687874f2a29cbdc8334e114af645c45298e85bb1efe69fcf15c617a0220575e'\n            b'40399f9ada388d8e522899f4ec3b7256896dd9b02742f6567d960b613f0401473044022024890462f731bd'\n            b'1a42a4716797bad94761fc4112e359117e591c07b8520ea33b02201ac689e35c4648e6beff1d42490207ba'\n            b'14027a638a62663b2ee40153299141eb014830450221009910823e0142967a73c2d16c1560054d71c0625a'\n            b'385904ba2f1f53e0bc1daa8d02205cd70a89c6cf031a8b07d1d5eb0d65d108c4d49c2d403f84fb03ad3dc3'\n            b'18777a014cad53210372ba1fd35e5f1b1437cba0c4ebfc4025b7349366f9f9c7c8c4b03a47bd3f68a42103'\n            b'061d250182b2db1ba144167fd8b0ef3fe0fc3a2fa046958f835ffaf0dfdb76922102463bfbc1eaec74b5c2'\n            b'1c09239ae18dbf6fc07833917df10d0b43e322810cee0c2102fa6a6455c26fb516cfa85ea8de81dd623a89'\n            b'3ffd579ee2a00deb6cdf3633d6bb210382910eae483ce4213d79d107bfc78f3d77e2a31ea597be45256171'\n            b'ad0abeaa8955ae'\n        )\n\n\nclass TestPayPubKeyHash(unittest.TestCase):\n\n    def pay_pubkey_hash(self, pubkey_hash):\n        # this checks that factory function correctly sets up the script\n        src1 = OutputScript.pay_pubkey_hash(unhexlify(pubkey_hash))\n        self.assertEqual(src1.template.name, 'pay_pubkey_hash')\n        self.assertEqual(hexlify(src1.values['pubkey_hash']), pubkey_hash)\n        # now we test that it will round trip\n        src2 = OutputScript(src1.source)\n        self.assertEqual(src2.template.name, 'pay_pubkey_hash')\n        self.assertEqual(hexlify(src2.values['pubkey_hash']), pubkey_hash)\n        return hexlify(src1.source)\n\n    def test_pay_pubkey_hash_1(self):\n        self.assertEqual(\n            self.pay_pubkey_hash(b'64d74d12acc93ba1ad495e8d2d0523252d664f4d'),\n            b'76a91464d74d12acc93ba1ad495e8d2d0523252d664f4d88ac'\n        )\n\n\nclass TestPayScriptHash(unittest.TestCase):\n\n    def pay_script_hash(self, script_hash):\n        # this checks that factory function correctly sets up the script\n        src1 = OutputScript.pay_script_hash(unhexlify(script_hash))\n        self.assertEqual(src1.template.name, 'pay_script_hash')\n        self.assertEqual(hexlify(src1.values['script_hash']), script_hash)\n        # now we test that it will round trip\n        src2 = OutputScript(src1.source)\n        self.assertEqual(src2.template.name, 'pay_script_hash')\n        self.assertEqual(hexlify(src2.values['script_hash']), script_hash)\n        return hexlify(src1.source)\n\n    def test_pay_pubkey_hash_1(self):\n        self.assertEqual(\n            self.pay_script_hash(b'63d65a2ee8c44426d06050cfd71c0f0ff3fc41ac'),\n            b'a91463d65a2ee8c44426d06050cfd71c0f0ff3fc41ac87'\n        )\n\n\nclass TestPayClaimNamePubkeyHash(unittest.TestCase):\n\n    def pay_claim_name_pubkey_hash(self, name, claim, pubkey_hash):\n        # this checks that factory function correctly sets up the script\n        src1 = OutputScript.pay_claim_name_pubkey_hash(\n            name, unhexlify(claim), unhexlify(pubkey_hash))\n        self.assertEqual(src1.template.name, 'claim_name+pay_pubkey_hash')\n        self.assertEqual(src1.values['claim_name'], name)\n        self.assertEqual(hexlify(src1.values['claim']), claim)\n        self.assertEqual(hexlify(src1.values['pubkey_hash']), pubkey_hash)\n        # now we test that it will round trip\n        src2 = OutputScript(src1.source)\n        self.assertEqual(src2.template.name, 'claim_name+pay_pubkey_hash')\n        self.assertEqual(src2.values['claim_name'], name)\n        self.assertEqual(hexlify(src2.values['claim']), claim)\n        self.assertEqual(hexlify(src2.values['pubkey_hash']), pubkey_hash)\n        return hexlify(src1.source)\n\n    def test_pay_claim_name_pubkey_hash_1(self):\n        self.assertEqual(\n            self.pay_claim_name_pubkey_hash(\n                # name\n                b'cats',\n                # claim\n                b'080110011a7808011230080410011a084d616361726f6e6922002a003214416c6c20726967687473'\n                b'2072657365727665642e38004a0052005a001a42080110011a30add80aaf02559ba09853636a0658'\n                b'c42b727cb5bb4ba8acedb4b7fe656065a47a31878dbf9912135ddb9e13806cc1479d220a696d6167'\n                b'652f6a7065672a5c080110031a404180cc0fa4d3839ee29cca866baed25fafb43fca1eb3b608ee88'\n                b'9d351d3573d042c7b83e2e643db0d8e062a04e6e9ae6b90540a2f95fe28638d0f18af4361a1c2214'\n                b'f73de93f4299fb32c32f949e02198a8e91101abd',\n                # pub key\n                b'be16e4b0f9bd8f6d47d02b3a887049c36d3b84cb'\n            ),\n            b'b504636174734cdc080110011a7808011230080410011a084d616361726f6e6922002a003214416c6c207'\n            b'269676874732072657365727665642e38004a0052005a001a42080110011a30add80aaf02559ba0985363'\n            b'6a0658c42b727cb5bb4ba8acedb4b7fe656065a47a31878dbf9912135ddb9e13806cc1479d220a696d616'\n            b'7652f6a7065672a5c080110031a404180cc0fa4d3839ee29cca866baed25fafb43fca1eb3b608ee889d35'\n            b'1d3573d042c7b83e2e643db0d8e062a04e6e9ae6b90540a2f95fe28638d0f18af4361a1c2214f73de93f4'\n            b'299fb32c32f949e02198a8e91101abd6d7576a914be16e4b0f9bd8f6d47d02b3a887049c36d3b84cb88ac'\n        )\n"
  },
  {
    "path": "tests/unit/wallet/test_stream_controller.py",
    "content": "from lbry.wallet.stream import StreamController\nfrom lbry.wallet.tasks import TaskGroup\nfrom lbry.testcase import AsyncioTestCase\n\n\nclass StreamControllerTestCase(AsyncioTestCase):\n    def test_non_unique_events(self):\n        events = []\n        controller = StreamController()\n        controller.stream.listen(on_data=events.append)\n        controller.add(\"yo\")\n        controller.add(\"yo\")\n        self.assertListEqual(events, [\"yo\", \"yo\"])\n\n    def test_unique_events(self):\n        events = []\n        controller = StreamController(merge_repeated_events=True)\n        controller.stream.listen(on_data=events.append)\n        controller.add(\"yo\")\n        controller.add(\"yo\")\n        self.assertListEqual(events, [\"yo\"])\n\n\nclass TaskGroupTestCase(AsyncioTestCase):\n\n    async def test_cancel_sets_it_done(self):\n        group = TaskGroup()\n        group.cancel()\n        self.assertTrue(group.done.is_set())\n"
  },
  {
    "path": "tests/unit/wallet/test_transaction.py",
    "content": "import os\nimport unittest\nimport tempfile\nimport shutil\nfrom binascii import hexlify, unhexlify\nfrom itertools import cycle\n\nfrom lbry.testcase import AsyncioTestCase\nfrom lbry.wallet.constants import CENT, COIN, NULL_HASH32\nfrom lbry.wallet import Wallet, Account, Ledger, Database, Headers, Transaction, Output, Input\n\n\nNULL_HASH = b'\\x00'*32\nFEE_PER_BYTE = 50\nFEE_PER_CHAR = 200000\n\n\ndef get_output(amount=CENT, pubkey_hash=NULL_HASH32, height=-2):\n    return Transaction(height=height) \\\n        .add_outputs([Output.pay_pubkey_hash(amount, pubkey_hash)]) \\\n        .outputs[0]\n\n\ndef get_input(amount=CENT, pubkey_hash=NULL_HASH):\n    return Input.spend(get_output(amount, pubkey_hash))\n\n\ndef get_transaction(txo=None):\n    return Transaction() \\\n        .add_inputs([get_input()]) \\\n        .add_outputs([txo or Output.pay_pubkey_hash(CENT, NULL_HASH32)])\n\n\ndef get_claim_transaction(claim_name, claim=b''):\n    return get_transaction(\n        Output.pay_claim_name_pubkey_hash(CENT, claim_name, claim, NULL_HASH32)\n    )\n\n\nclass TestSizeAndFeeEstimation(AsyncioTestCase):\n\n    async def asyncSetUp(self):\n        self.ledger = Ledger({\n            'db': Database(':memory:'),\n            'headers': Headers(':memory:'),\n            'fee_per_name_char': 200_000\n        })\n        await self.ledger.db.open()\n\n    async def asyncTearDown(self):\n        await self.ledger.db.close()\n\n    def test_output_size_and_fee(self):\n        txo = get_output()\n        self.assertEqual(txo.size, 46)\n        self.assertEqual(txo.get_fee(self.ledger), 46 * FEE_PER_BYTE)\n        claim_name = 'verylongname'\n        tx = get_claim_transaction(claim_name, b'0'*4000)\n        base_size = tx.size - tx.inputs[0].size - tx.outputs[0].size\n        txo = tx.outputs[0]\n        self.assertEqual(tx.size, 4225)\n        self.assertEqual(tx.base_size, base_size)\n        self.assertEqual(txo.size, 4067)\n        self.assertEqual(txo.get_fee(self.ledger), len(claim_name) * FEE_PER_CHAR)\n        # fee based on total bytes is the larger fee\n        claim_name = 'a'\n        tx = get_claim_transaction(claim_name, b'0'*4000)\n        base_size = tx.size - tx.inputs[0].size - tx.outputs[0].size\n        txo = tx.outputs[0]\n        self.assertEqual(tx.size, 4214)\n        self.assertEqual(tx.base_size, base_size)\n        self.assertEqual(txo.size, 4056)\n        self.assertEqual(txo.get_fee(self.ledger), txo.size * FEE_PER_BYTE)\n\n    def test_input_size_and_fee(self):\n        txi = get_input()\n        self.assertEqual(txi.size, 148)\n        self.assertEqual(txi.get_fee(self.ledger), 148 * FEE_PER_BYTE)\n\n    def test_transaction_size_and_fee(self):\n        tx = get_transaction()\n        self.assertEqual(tx.size, 204)\n        self.assertEqual(tx.base_size, tx.size - tx.inputs[0].size - tx.outputs[0].size)\n        self.assertEqual(tx.get_base_fee(self.ledger), FEE_PER_BYTE * tx.base_size)\n\n\nclass TestAccountBalanceImpactFromTransaction(unittest.TestCase):\n\n    def test_is_my_output_not_set(self):\n        tx = get_transaction()\n        with self.assertRaisesRegex(ValueError, \"Cannot access net_account_balance\"):\n            _ = tx.net_account_balance\n        tx.inputs[0].txo_ref.txo.is_my_output = True\n        with self.assertRaisesRegex(ValueError, \"Cannot access net_account_balance\"):\n            _ = tx.net_account_balance\n        tx.outputs[0].is_my_output = True\n        # all inputs/outputs are set now so it should work\n        _ = tx.net_account_balance\n\n    def test_paying_from_my_account_to_other_account(self):\n        tx = Transaction() \\\n            .add_inputs([get_input(300*CENT)]) \\\n            .add_outputs([get_output(190*CENT, NULL_HASH),\n                          get_output(100*CENT, NULL_HASH)])\n        tx.inputs[0].txo_ref.txo.is_my_output = True\n        tx.outputs[0].is_my_output = False\n        tx.outputs[1].is_my_output = True\n        self.assertEqual(tx.net_account_balance, -200*CENT)\n\n    def test_paying_from_other_account_to_my_account(self):\n        tx = Transaction() \\\n            .add_inputs([get_input(300*CENT)]) \\\n            .add_outputs([get_output(190*CENT, NULL_HASH),\n                          get_output(100*CENT, NULL_HASH)])\n        tx.inputs[0].txo_ref.txo.is_my_output = False\n        tx.outputs[0].is_my_output = True\n        tx.outputs[1].is_my_output = False\n        self.assertEqual(tx.net_account_balance, 190*CENT)\n\n    def test_paying_from_my_account_to_my_account(self):\n        tx = Transaction() \\\n            .add_inputs([get_input(300*CENT)]) \\\n            .add_outputs([get_output(190*CENT, NULL_HASH),\n                          get_output(100*CENT, NULL_HASH)])\n        tx.inputs[0].txo_ref.txo.is_my_output = True\n        tx.outputs[0].is_my_output = True\n        tx.outputs[1].is_my_output = True\n        self.assertEqual(tx.net_account_balance, -10*CENT)  # lost to fee\n\n\nclass TestTransactionSerialization(unittest.TestCase):\n\n    def test_genesis_transaction(self):\n        raw = unhexlify(\n            \"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1f0\"\n            \"4ffff001d010417696e736572742074696d657374616d7020737472696e67ffffffff01000004bfc91b8e\"\n            \"001976a914345991dbf57bfb014b87006acdfafbfc5fe8292f88ac00000000\"\n        )\n        tx = Transaction(raw)\n        self.assertEqual(tx.version, 1)\n        self.assertEqual(tx.locktime, 0)\n        self.assertEqual(len(tx.inputs), 1)\n        self.assertEqual(len(tx.outputs), 1)\n\n        coinbase = tx.inputs[0]\n        self.assertTrue(coinbase.txo_ref.is_null)\n        self.assertEqual(coinbase.txo_ref.position, 0xFFFFFFFF)\n        self.assertEqual(coinbase.sequence, 0xFFFFFFFF)\n        self.assertIsNotNone(coinbase.coinbase)\n        self.assertIsNone(coinbase.script)\n        self.assertEqual(\n            hexlify(coinbase.coinbase),\n            b'04ffff001d010417696e736572742074696d657374616d7020737472696e67'\n        )\n\n        out = tx.outputs[0]\n        self.assertEqual(out.amount, 40000000000000000)\n        self.assertEqual(out.position, 0)\n        self.assertTrue(out.script.is_pay_pubkey_hash)\n        self.assertFalse(out.script.is_pay_script_hash)\n        self.assertFalse(out.script.is_claim_involved)\n\n        tx._reset()\n        self.assertEqual(tx.raw, raw)\n\n    def test_coinbase_transaction(self):\n        raw = unhexlify(\n            \"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff200\"\n            \"34d520504f89ac55a086032d217bf0700000d2f6e6f64655374726174756d2f0000000001a03489850800\"\n            \"00001976a914cfab870d6deea54ca94a41912a75484649e52f2088ac00000000\"\n        )\n        tx = Transaction(raw)\n        self.assertEqual(tx.version, 1)\n        self.assertEqual(tx.locktime, 0)\n        self.assertEqual(len(tx.inputs), 1)\n        self.assertEqual(len(tx.outputs), 1)\n\n        coinbase = tx.inputs[0]\n        self.assertTrue(coinbase.txo_ref.is_null)\n        self.assertEqual(coinbase.txo_ref.position, 0xFFFFFFFF)\n        self.assertEqual(coinbase.sequence, 0)\n        self.assertIsNotNone(coinbase.coinbase)\n        self.assertIsNone(coinbase.script)\n        self.assertEqual(\n            hexlify(coinbase.coinbase),\n            b'034d520504f89ac55a086032d217bf0700000d2f6e6f64655374726174756d2f'\n        )\n\n        out = tx.outputs[0]\n        self.assertEqual(out.amount, 36600100000)\n        self.assertEqual(out.position, 0)\n        self.assertTrue(out.script.is_pay_pubkey_hash)\n        self.assertFalse(out.script.is_pay_script_hash)\n        self.assertFalse(out.script.is_claim_involved)\n\n        tx._reset()\n        self.assertEqual(tx.raw, raw)\n\n    def test_claim_transaction(self):\n        raw = unhexlify(\n            \"01000000012433e1b327603843b083344dbae5306ff7927f87ebbc5ae9eb50856c5b53fd1d000000006a4\"\n            \"7304402201a91e1023d11c383a11e26bf8f9034087b15d8ada78fa565e0610455ffc8505e0220038a63a6\"\n            \"ecb399723d4f1f78a20ddec0a78bf8fb6c75e63e166ef780f3944fbf0121021810150a2e4b088ec51b20c\"\n            \"be1b335962b634545860733367824d5dc3eda767dffffffff028096980000000000fdff00b50463617473\"\n            \"4cdc080110011a7808011230080410011a084d616361726f6e6922002a003214416c6c207269676874732\"\n            \"072657365727665642e38004a0052005a001a42080110011a30add80aaf02559ba09853636a0658c42b72\"\n            \"7cb5bb4ba8acedb4b7fe656065a47a31878dbf9912135ddb9e13806cc1479d220a696d6167652f6a70656\"\n            \"72a5c080110031a404180cc0fa4d3839ee29cca866baed25fafb43fca1eb3b608ee889d351d3573d042c7\"\n            \"b83e2e643db0d8e062a04e6e9ae6b90540a2f95fe28638d0f18af4361a1c2214f73de93f4299fb32c32f9\"\n            \"49e02198a8e91101abd6d7576a914be16e4b0f9bd8f6d47d02b3a887049c36d3b84cb88ac0cd2520b0000\"\n            \"00001976a914f521178feb733a719964e1da4a9efb09dcc39cfa88ac00000000\"\n        )\n        tx = Transaction(raw)\n        self.assertEqual(tx.id, '666c3d15de1d6949a4fe717126c368e274b36957dce29fd401138c1e87e92a62')\n        self.assertEqual(tx.version, 1)\n        self.assertEqual(tx.locktime, 0)\n        self.assertEqual(len(tx.inputs), 1)\n        self.assertEqual(len(tx.outputs), 2)\n\n        txin = tx.inputs[0]\n        self.assertEqual(\n            txin.txo_ref.id,\n            '1dfd535b6c8550ebe95abceb877f92f76f30e5ba4d3483b043386027b3e13324:0'\n        )\n        self.assertEqual(txin.txo_ref.position, 0)\n        self.assertEqual(txin.sequence, 0xFFFFFFFF)\n        self.assertIsNone(txin.coinbase)\n        self.assertEqual(txin.script.template.name, 'pubkey_hash')\n        self.assertEqual(\n            hexlify(txin.script.values['pubkey']),\n            b'021810150a2e4b088ec51b20cbe1b335962b634545860733367824d5dc3eda767d'\n        )\n        self.assertEqual(\n            hexlify(txin.script.values['signature']),\n            b'304402201a91e1023d11c383a11e26bf8f9034087b15d8ada78fa565e0610455ffc8505e0220038a63a6'\n            b'ecb399723d4f1f78a20ddec0a78bf8fb6c75e63e166ef780f3944fbf01'\n        )\n\n        # Claim\n        out0 = tx.outputs[0]\n        self.assertEqual(out0.amount, 10000000)\n        self.assertEqual(out0.position, 0)\n        self.assertTrue(out0.script.is_pay_pubkey_hash)\n        self.assertTrue(out0.script.is_claim_name)\n        self.assertTrue(out0.script.is_claim_involved)\n        self.assertEqual(out0.script.values['claim_name'], b'cats')\n        self.assertEqual(\n            hexlify(out0.script.values['pubkey_hash']),\n            b'be16e4b0f9bd8f6d47d02b3a887049c36d3b84cb'\n        )\n\n        # Change\n        out1 = tx.outputs[1]\n        self.assertEqual(out1.amount, 189977100)\n        self.assertEqual(out1.position, 1)\n        self.assertTrue(out1.script.is_pay_pubkey_hash)\n        self.assertFalse(out1.script.is_claim_involved)\n        self.assertEqual(\n            hexlify(out1.script.values['pubkey_hash']),\n            b'f521178feb733a719964e1da4a9efb09dcc39cfa'\n        )\n\n        tx._reset()\n        self.assertEqual(tx.raw, raw)\n\n    def test_redeem_scripthash_transaction(self):\n        raw = unhexlify(\n            \"0200000001409223c2405238fdc516d4f2e8aa57637ce52d3b1ac42b26f1accdcda9697e79010000008a4\"\n            \"730440220033d5286f161da717d9d1bc3c2bc28da7636b38fc0c6aefb1e0864212f05282c02205df3ce13\"\n            \"5e79c76d44489212f77ad4e3a838562e601e6377704fa6206a6ae44f012102261773e7eebe9da80a5653d\"\n            \"865cc600362f8e7b2b598661139dd902b5b01ea101f03aaf30ab17576a914a3328f18ac1892a6667f713d\"\n            \"7020ff3437d973c888acfeffffff0180ed3e17000000001976a914353352b7ce1e3c9c05ffcd6ae97609d\"\n            \"e2999744488accdf50a00\"\n        )\n        tx = Transaction(raw)\n        self.assertEqual(tx.id, 'e466881128889d1cc4110627753051c22e72a81d11229a1a1337da06940bebcf')\n        self.assertEqual(tx.version, 2)\n        self.assertEqual(tx.locktime, 718285,)\n        self.assertEqual(len(tx.inputs), 1)\n        self.assertEqual(len(tx.outputs), 1)\n\n        txin = tx.inputs[0]\n        self.assertEqual(\n            txin.txo_ref.id,\n            '797e69a9cdcdacf1262bc41a3b2de57c6357aae8f2d416c5fd385240c2239240:1'\n        )\n        self.assertEqual(txin.txo_ref.position, 1)\n        self.assertEqual(txin.sequence, 4294967294)\n        self.assertIsNone(txin.coinbase)\n        self.assertEqual(txin.script.template.name, 'script_hash+timelock')\n        self.assertEqual(\n            hexlify(txin.script.values['signature']),\n            b'30440220033d5286f161da717d9d1bc3c2bc28da7636b38fc0c6aefb1e0864212f'\n            b'05282c02205df3ce135e79c76d44489212f77ad4e3a838562e601e6377704fa620'\n            b'6a6ae44f01'\n        )\n        self.assertEqual(\n            hexlify(txin.script.values['pubkey']),\n            b'02261773e7eebe9da80a5653d865cc600362f8e7b2b598661139dd902b5b01ea10'\n        )\n        script = txin.script.values['script']\n        self.assertEqual(script.template.name, 'timelock')\n        self.assertEqual(script.values['height'], 717738)\n        self.assertEqual(hexlify(script.values['pubkey_hash']), b'a3328f18ac1892a6667f713d7020ff3437d973c8')\n\n\nclass TestTransactionSigning(AsyncioTestCase):\n\n    async def asyncSetUp(self):\n        self.ledger = Ledger({\n            'db': Database(':memory:'),\n            'headers': Headers(':memory:')\n        })\n        await self.ledger.db.open()\n\n    async def asyncTearDown(self):\n        await self.ledger.db.close()\n\n    async def test_sign(self):\n        account = Account.from_dict(\n            self.ledger, Wallet(), {\n                \"seed\":\n                    \"carbon smart garage balance margin twelve chest sword toas\"\n                    \"t envelope bottom stomach absent\"\n            }\n        )\n\n        await account.ensure_address_gap()\n        address1, address2 = await account.receiving.get_addresses(limit=2)\n        pubkey_hash1 = self.ledger.address_to_hash160(address1)\n        pubkey_hash2 = self.ledger.address_to_hash160(address2)\n\n        tx = Transaction() \\\n            .add_inputs([Input.spend(get_output(int(2*COIN), pubkey_hash1))]) \\\n            .add_outputs([Output.pay_pubkey_hash(int(1.9*COIN), pubkey_hash2)])\n\n        await tx.sign([account])\n\n        self.assertEqual(\n            hexlify(tx.inputs[0].script.values['signature']),\n            b'304402200dafa26ad7cf38c5a971c8a25ce7d85a076235f146126762296b1223c42ae21e022020ef9eeb8'\n            b'398327891008c5c0be4357683f12cb22346691ff23914f457bf679601'\n        )\n\n\nclass TransactionIOBalancing(AsyncioTestCase):\n\n    async def asyncSetUp(self):\n        wallet_dir = tempfile.mkdtemp()\n        self.addCleanup(shutil.rmtree, wallet_dir)\n        self.ledger = Ledger({\n            'db': Database(os.path.join(wallet_dir, 'blockchain.db')),\n            'headers': Headers(':memory:'),\n        })\n        await self.ledger.db.open()\n        self.account = Account.from_dict(\n            self.ledger, Wallet(), {\n                \"seed\": \"carbon smart garage balance margin twelve chest sword \"\n                        \"toast envelope bottom stomach absent\"\n            }\n        )\n\n        addresses = await self.account.ensure_address_gap()\n        self.pubkey_hash = [self.ledger.address_to_hash160(a) for a in addresses]\n        self.hash_cycler = cycle(self.pubkey_hash)\n\n    async def asyncTearDown(self):\n        await self.ledger.db.close()\n\n    def txo(self, amount, address=None):\n        return get_output(int(amount*COIN), address or next(self.hash_cycler))\n\n    def txi(self, txo):\n        return Input.spend(txo)\n\n    def tx(self, inputs, outputs):\n        return Transaction.create(inputs, outputs, [self.account], self.account)\n\n    async def create_utxos(self, amounts):\n        utxos = [self.txo(amount) for amount in amounts]\n\n        self.funding_tx = Transaction(is_verified=True) \\\n            .add_inputs([self.txi(self.txo(sum(amounts)+0.1))]) \\\n            .add_outputs(utxos)\n\n        await self.ledger.db.insert_transaction(self.funding_tx)\n\n        for utxo in utxos:\n            await self.ledger.db.save_transaction_io(\n                self.funding_tx,\n                self.ledger.hash160_to_address(utxo.script.values['pubkey_hash']),\n                utxo.script.values['pubkey_hash'], ''\n            )\n\n        return utxos\n\n    @staticmethod\n    def inputs(tx):\n        return [round(i.amount/COIN, 2) for i in tx.inputs]\n\n    @staticmethod\n    def outputs(tx):\n        return [round(o.amount/COIN, 2) for o in tx.outputs]\n\n    async def test_basic_use_cases(self):\n        self.ledger.fee_per_byte = int(.01*CENT)\n\n        # available UTXOs for filling missing inputs\n        utxos = await self.create_utxos([\n            1, 1, 3, 5, 10\n        ])\n\n        # pay 3 coins (3.02 w/ fees)\n        tx = await self.tx(\n            [],            # inputs\n            [self.txo(3)]  # outputs\n        )\n        # best UTXO match is 5 (as UTXO 3 will be short 0.02 to cover fees)\n        self.assertListEqual(self.inputs(tx), [5])\n        # a change of 1.98 is added to reach balance\n        self.assertListEqual(self.outputs(tx), [3, 1.98])\n\n        await self.ledger.release_outputs(utxos)\n\n        # pay 2.98 coins (3.00 w/ fees)\n        tx = await self.tx(\n            [],               # inputs\n            [self.txo(2.98)]  # outputs\n        )\n        # best UTXO match is 3 and no change is needed\n        self.assertListEqual(self.inputs(tx), [3])\n        self.assertListEqual(self.outputs(tx), [2.98])\n\n        await self.ledger.release_outputs(utxos)\n\n        # supplied input and output, but input is not enough to cover output\n        tx = await self.tx(\n            [self.txi(self.txo(10))],  # inputs\n            [self.txo(11)]             # outputs\n        )\n        # additional input is chosen (UTXO 3)\n        self.assertListEqual([10, 3], self.inputs(tx))\n        # change is now needed to consume extra input\n        self.assertListEqual([11, 1.96], self.outputs(tx))\n\n        await self.ledger.release_outputs(utxos)\n\n        # liquidating a UTXO\n        tx = await self.tx(\n            [self.txi(self.txo(10))],  # inputs\n            []                         # outputs\n        )\n        self.assertListEqual([10], self.inputs(tx))\n        # missing change added to consume the amount\n        self.assertListEqual([9.98], self.outputs(tx))\n\n        await self.ledger.release_outputs(utxos)\n\n        # liquidating at a loss, requires adding extra inputs\n        tx = await self.tx(\n            [self.txi(self.txo(0.01))],  # inputs\n            []                           # outputs\n        )\n        # UTXO 1 is added to cover some of the fee\n        self.assertListEqual([0.01, 1], self.inputs(tx))\n        # change is now needed to consume extra input\n        self.assertListEqual([0.97], self.outputs(tx))\n\n    async def test_basic_use_cases_sqlite(self):\n        self.ledger.coin_selection_strategy = 'sqlite'\n        self.ledger.fee_per_byte = int(0.01*CENT)\n\n        # available UTXOs for filling missing inputs\n        utxos = await self.create_utxos([\n            1, 1, 3, 5, 10\n        ])\n\n        self.assertEqual(5, len(await self.ledger.get_utxos()))\n\n        # pay 3 coins (3.07 w/ fees)\n        tx = await self.tx(\n            [],            # inputs\n            [self.txo(3)]  # outputs\n        )\n\n        await self.ledger.db.db.run(self.ledger.db._transaction_io, tx, tx.outputs[0].get_address(self.ledger), tx.id)\n\n        self.assertListEqual(self.inputs(tx), [1.0, 1.0, 3.0])\n        # a change of 1.95 is added to reach balance\n        self.assertListEqual(self.outputs(tx), [3, 1.95])\n        # utxos: 1.95, 3, 5, 10\n        self.assertEqual(2, len(await self.ledger.get_utxos()))\n        # pay 4.946 coins (5.00 w/ fees)\n        tx = await self.tx(\n            [],                # inputs\n            [self.txo(4.946)]  # outputs\n        )\n        self.assertEqual(1, len(await self.ledger.get_utxos()))\n\n        self.assertListEqual(self.inputs(tx), [5.0])\n        self.assertEqual(2, len(tx.outputs))\n        self.assertEqual(494600000, tx.outputs[0].amount)\n\n        # utxos: 3, 1.95, 4.946, 10\n        await self.ledger.release_outputs(utxos)\n\n        # supplied input and output, but input is not enough to cover output\n        tx = await self.tx(\n            [self.txi(self.txo(10))],  # inputs\n            [self.txo(11)]             # outputs\n        )\n        # additional input is chosen (UTXO 1)\n        self.assertListEqual([10, 1.0, 1.0], self.inputs(tx))\n        # change is now needed to consume extra input\n        self.assertListEqual([11, 0.95], self.outputs(tx))\n        await self.ledger.release_outputs(utxos)\n        # liquidating a UTXO\n        tx = await self.tx(\n            [self.txi(self.txo(10))],  # inputs\n            []                         # outputs\n        )\n        self.assertListEqual([10], self.inputs(tx))\n        # missing change added to consume the amount\n        self.assertListEqual([9.98], self.outputs(tx))\n        await self.ledger.release_outputs(utxos)\n        # liquidating at a loss, requires adding extra inputs\n        tx = await self.tx(\n            [self.txi(self.txo(0.01))],  # inputs\n            []                           # outputs\n        )\n        # UTXO 1 is added to cover some of the fee\n        self.assertListEqual([0.01, 1], self.inputs(tx))\n        # change is now needed to consume extra input\n        self.assertListEqual([0.97], self.outputs(tx))\n"
  },
  {
    "path": "tests/unit/wallet/test_utils.py",
    "content": "import unittest\n\nfrom lbry.wallet.util import ArithUint256\nfrom lbry.wallet.util import coins_to_satoshis as c2s, satoshis_to_coins as s2c\n\n\nclass TestCoinValueParsing(unittest.TestCase):\n\n    def test_good_output(self):\n        self.assertEqual(s2c(1), \"0.00000001\")\n        self.assertEqual(s2c(10**7), \"0.1\")\n        self.assertEqual(s2c(2*10**8), \"2.0\")\n        self.assertEqual(s2c(2*10**17), \"2000000000.0\")\n\n    def test_good_input(self):\n        self.assertEqual(c2s(\"0.00000001\"), 1)\n        self.assertEqual(c2s(\"0.1\"), 10**7)\n        self.assertEqual(c2s(\"1.0\"), 10**8)\n        self.assertEqual(c2s(\"2.00000000\"), 2*10**8)\n        self.assertEqual(c2s(\"2000000000.0\"), 2*10**17)\n\n    def test_bad_input(self):\n        with self.assertRaises(ValueError):\n            c2s(\"1\")\n        with self.assertRaises(ValueError):\n            c2s(\"-1.0\")\n        with self.assertRaises(ValueError):\n            c2s(\"10000000000.0\")\n        with self.assertRaises(ValueError):\n            c2s(\"1.000000000\")\n        with self.assertRaises(ValueError):\n            c2s(\"-0\")\n        with self.assertRaises(ValueError):\n            c2s(\"1\")\n        with self.assertRaises(ValueError):\n            c2s(\".1\")\n        with self.assertRaises(ValueError):\n            c2s(\"1e-7\")\n\n\nclass TestArithUint256(unittest.TestCase):\n\n    def test_arithunit256(self):\n        # https://github.com/bitcoin/bitcoin/blob/master/src/test/arith_uint256_tests.cpp\n\n        from_compact = ArithUint256.from_compact\n        eq = self.assertEqual\n\n        eq(from_compact(0).value, 0)\n        eq(from_compact(0x00123456).value, 0)\n        eq(from_compact(0x01003456).value, 0)\n        eq(from_compact(0x02000056).value, 0)\n        eq(from_compact(0x03000000).value, 0)\n        eq(from_compact(0x04000000).value, 0)\n        eq(from_compact(0x00923456).value, 0)\n        eq(from_compact(0x01803456).value, 0)\n        eq(from_compact(0x02800056).value, 0)\n        eq(from_compact(0x03800000).value, 0)\n        eq(from_compact(0x04800000).value, 0)\n\n        # Make sure that we don't generate compacts with the 0x00800000 bit set\n        uint = ArithUint256(0x80)\n        eq(uint.compact,  0x02008000)\n\n        uint = from_compact(0x01123456)\n        eq(uint.value, 0x12)\n        eq(uint.compact, 0x01120000)\n\n        uint = from_compact(0x01fedcba)\n        eq(uint.value, 0x7e)\n        eq(uint.negative, 0x01fe0000)\n\n        uint = from_compact(0x02123456)\n        eq(uint.value, 0x1234)\n        eq(uint.compact, 0x02123400)\n\n        uint = from_compact(0x03123456)\n        eq(uint.value, 0x123456)\n        eq(uint.compact, 0x03123456)\n\n        uint = from_compact(0x04123456)\n        eq(uint.value, 0x12345600)\n        eq(uint.compact, 0x04123456)\n\n        uint = from_compact(0x04923456)\n        eq(uint.value, 0x12345600)\n        eq(uint.negative, 0x04923456)\n\n        uint = from_compact(0x05009234)\n        eq(uint.value, 0x92340000)\n        eq(uint.compact, 0x05009234)\n\n        uint = from_compact(0x20123456)\n        eq(uint.value, 0x1234560000000000000000000000000000000000000000000000000000000000)\n        eq(uint.compact, 0x20123456)\n"
  },
  {
    "path": "tests/unit/wallet/test_wallet.py",
    "content": "import json\nimport jsonschema\nimport os\nimport tempfile\nfrom binascii import hexlify\n\nimport lbry.schema.types.v2 as schema_v2\nfrom unittest import TestCase, mock\nfrom lbry.testcase import AsyncioTestCase\nfrom lbry.wallet import (\n    Ledger, RegTestLedger, WalletManager, Account,\n    Wallet, WalletStorage, TimestampedPreferences\n)\n\n\nclass TestWalletCreation(AsyncioTestCase):\n\n    async def asyncSetUp(self):\n        self.manager = WalletManager()\n        config = {'data_path': '/tmp/wallet'}\n        self.main_ledger = self.manager.get_or_create_ledger(Ledger.get_id(), config)\n        self.test_ledger = self.manager.get_or_create_ledger(RegTestLedger.get_id(), config)\n\n    def test_create_wallet_and_accounts(self):\n        wallet = Wallet()\n        self.assertEqual(wallet.name, 'Wallet')\n        self.assertListEqual(wallet.accounts, [])\n\n        account1 = wallet.generate_account(self.main_ledger)\n        wallet.generate_account(self.main_ledger)\n        wallet.generate_account(self.test_ledger)\n        self.assertEqual(wallet.default_account, account1)\n        self.assertEqual(len(wallet.accounts), 3)\n\n    def test_load_and_save_wallet(self):\n        wallet_dict = {\n            'version': 1,\n            'name': 'Main Wallet',\n            'preferences': {},\n            'accounts': [\n                {\n                    'certificates': {},\n                    'name': 'An Account',\n                    'ledger': 'lbc_mainnet',\n                    'modified_on': 123,\n                    'seed':\n                        \"carbon smart garage balance margin twelve chest sword toast envelope bottom stomac\"\n                        \"h absent\",\n                    'encrypted': False,\n                    'private_key':\n                        'xprv9s21ZrQH143K42ovpZygnjfHdAqSd9jo7zceDfPRogM7bkkoNVv7'\n                        'DRNLEoB8HoirMgH969NrgL8jNzLEegqFzPRWM37GXd4uE8uuRkx4LAe',\n                    'public_key':\n                        'xpub661MyMwAqRbcGWtPvbWh9sc2BCfw2cTeVDYF23o3N1t6UZ5wv3EMm'\n                        'Dgp66FxHuDtWdft3B5eL5xQtyzAtkdmhhC95gjRjLzSTdkho95asu9',\n                    'address_generator': {\n                        'name': 'deterministic-chain',\n                        'receiving': {'gap': 17, 'maximum_uses_per_address': 3},\n                        'change': {'gap': 10, 'maximum_uses_per_address': 3}\n                    }\n                }\n            ]\n        }\n\n        storage = WalletStorage(default=wallet_dict)\n        wallet = Wallet.from_storage(storage, self.manager)\n        self.assertEqual(wallet.name, 'Main Wallet')\n        self.assertEqual(\n            hexlify(wallet.hash), b'869acc4660dde0f13784ed743796adf89562cdf79fdfc9e5c6dbea98d62ccf90'\n        )\n        self.assertEqual(len(wallet.accounts), 1)\n        account = wallet.default_account\n        self.assertIsInstance(account, Account)\n        self.maxDiff = None\n        self.assertDictEqual(wallet_dict, wallet.to_dict())\n\n        encrypted = wallet.pack('password')\n        decrypted = Wallet.unpack('password', encrypted)\n        self.assertEqual(decrypted['accounts'][0]['name'], 'An Account')\n\n    def test_wallet_file_schema(self):\n        wallet_dict = {\n            'version': 1,\n            'name': 'Main Wallet',\n            'preferences': {},\n            'accounts': [\n                {\n                    'certificates': {'x': 'y'},\n                    'name': 'Account 1',\n                    'ledger': 'lbc_mainnet',\n                    'modified_on': 123,\n                    'seed':\n                        \"carbon smart garage balance margin twelve chest sword toast envelope bottom stomac\"\n                        \"h absent\",\n                    'encrypted': False,\n                    'private_key':\n                        'xprv9s21ZrQH143K42ovpZygnjfHdAqSd9jo7zceDfPRogM7bkkoNVv7'\n                        'DRNLEoB8HoirMgH969NrgL8jNzLEegqFzPRWM37GXd4uE8uuRkx4LAe',\n                    'public_key':\n                        'xpub661MyMwAqRbcGWtPvbWh9sc2BCfw2cTeVDYF23o3N1t6UZ5wv3EMm'\n                        'Dgp66FxHuDtWdft3B5eL5xQtyzAtkdmhhC95gjRjLzSTdkho95asu9',\n                    'address_generator': {\n                        'name': 'deterministic-chain',\n                        'receiving': {'gap': 17, 'maximum_uses_per_address': 3},\n                        'change': {'gap': 10, 'maximum_uses_per_address': 3}\n                    }\n                },\n                {\n                    'certificates': {'a': 'b'},\n                    'name': 'Account 2',\n                    'ledger': 'lbc_mainnet',\n                    'modified_on': 123,\n                    'seed':\n                        \"carbon smart garage balance margin twelve chest sword toast envelope bottom stomac\"\n                        \"h absent\",\n                    'encrypted': True,\n                    'private_key':\n                        'xprv9s21ZrQH143K42ovpZygnjfHdAqSd9jo7zceDfPRogM7bkkoNVv7'\n                        'DRNLEoB8HoirMgH969NrgL8jNzLEegqFzPRWM37GXd4uE8uuRkx4LAe',\n                    'public_key':\n                        'xpub661MyMwAqRbcGWtPvbWh9sc2BCfw2cTeVDYF23o3N1t6UZ5wv3EMm'\n                        'Dgp66FxHuDtWdft3B5eL5xQtyzAtkdmhhC95gjRjLzSTdkho95asu9',\n                    'address_generator': {\n                        'name': 'single-address',\n                    }\n                },\n            ]\n        }\n\n        storage = WalletStorage(default=wallet_dict)\n        wallet = Wallet.from_storage(storage, self.manager)\n        self.assertDictEqual(wallet_dict, wallet.to_dict())\n        with open(os.path.join(*schema_v2.__path__, 'wallet.json')) as f:\n            wallet_schema = json.load(f)\n        jsonschema.validate(schema=wallet_schema, instance=wallet.to_dict())\n\n    def test_no_password_but_encryption_preferred(self):\n        wallet_dict = {\n            'version': 1,\n            'name': 'Main Wallet',\n            'preferences': {\n                \"encrypt-on-disk\": {\n                    \"ts\": 1571762543.351794,\n                    \"value\": True\n                },\n            },\n            'accounts': [\n                {\n                    'certificates': {},\n                    'name': 'An Account',\n                    'ledger': 'lbc_mainnet',\n                    'modified_on': 123,\n                    'seed':\n                        \"carbon smart garage balance margin twelve chest sword toast envelope bottom stomac\"\n                        \"h absent\",\n                    'encrypted': False,\n                    'private_key':\n                        'xprv9s21ZrQH143K42ovpZygnjfHdAqSd9jo7zceDfPRogM7bkkoNVv7'\n                        'DRNLEoB8HoirMgH969NrgL8jNzLEegqFzPRWM37GXd4uE8uuRkx4LAe',\n                    'public_key':\n                        'xpub661MyMwAqRbcGWtPvbWh9sc2BCfw2cTeVDYF23o3N1t6UZ5wv3EMm'\n                        'Dgp66FxHuDtWdft3B5eL5xQtyzAtkdmhhC95gjRjLzSTdkho95asu9',\n                    'address_generator': {\n                        'name': 'deterministic-chain',\n                        'receiving': {'gap': 17, 'maximum_uses_per_address': 3},\n                        'change': {'gap': 10, 'maximum_uses_per_address': 3}\n                    }\n                }\n            ]\n        }\n\n        storage = WalletStorage(default=wallet_dict)\n        wallet = Wallet.from_storage(storage, self.manager)\n        self.assertEqual(\n            hexlify(wallet.hash), b'8cc6341885e6ad46f72a17364c65f8441f09e79996c55202196b399c75f8d751'\n        )\n        self.assertFalse(wallet.is_encrypted)\n\n    def test_read_write(self):\n        manager = WalletManager()\n        config = {'data_path': '/tmp/wallet'}\n        ledger = manager.get_or_create_ledger(Ledger.get_id(), config)\n\n        with tempfile.NamedTemporaryFile(suffix='.json') as wallet_file:\n            wallet_file.write(b'{\"version\": 1}')\n            wallet_file.seek(0)\n\n            # create and write wallet to a file\n            wallet = manager.import_wallet(wallet_file.name)\n            account = wallet.generate_account(ledger)\n            wallet.save()\n\n            # read wallet from file\n            wallet_storage = WalletStorage(wallet_file.name)\n            wallet = Wallet.from_storage(wallet_storage, manager)\n\n            self.assertEqual(account.public_key.address, wallet.default_account.public_key.address)\n\n    def test_merge(self):\n        wallet1 = Wallet()\n        wallet1.preferences['one'] = 1\n        wallet1.preferences['conflict'] = 1\n        wallet1.generate_account(self.main_ledger)\n        wallet2 = Wallet()\n        wallet2.preferences['two'] = 2\n        wallet2.preferences['conflict'] = 2  # will be more recent\n        wallet2.generate_account(self.main_ledger)\n\n        self.assertEqual(len(wallet1.accounts), 1)\n        self.assertEqual(wallet1.preferences, {'one': 1, 'conflict': 1})\n\n        added, _ = wallet1.merge(self.manager, 'password', wallet2.pack('password'))\n        self.assertEqual(added[0].id, wallet2.default_account.id)\n        self.assertEqual(len(wallet1.accounts), 2)\n        self.assertEqual(wallet1.accounts[1].id, wallet2.default_account.id)\n        self.assertEqual(wallet1.preferences, {'one': 1, 'two': 2, 'conflict': 2})\n\n\nclass TestTimestampedPreferences(TestCase):\n\n    def test_init(self):\n        p = TimestampedPreferences()\n        p['one'] = 1\n        p2 = TimestampedPreferences(p.data)\n        self.assertEqual(p2['one'], 1)\n\n    def test_hash(self):\n        p = TimestampedPreferences()\n        self.assertEqual(\n            hexlify(p.hash), b'44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a'\n        )\n        with mock.patch('time.time', mock.Mock(return_value=12345)):\n            p['one'] = 1\n        self.assertEqual(\n            hexlify(p.hash), b'c9e82bf4cb099dd0125f78fa381b21a8131af601917eb531e1f5f980f8f3da66'\n        )\n\n    def test_merge(self):\n        p1 = TimestampedPreferences()\n        p2 = TimestampedPreferences()\n        with mock.patch('time.time', mock.Mock(return_value=10)):\n            p1['one'] = 1\n            p1['conflict'] = 1\n        with mock.patch('time.time', mock.Mock(return_value=20)):\n            p2['two'] = 2\n            p2['conflict'] = 2\n\n        # conflict in p2 overrides conflict in p1\n        p1.merge(p2.data)\n        self.assertEqual(p1, {'one': 1, 'two': 2, 'conflict': 2})\n\n        # have a newer conflict in p1 so it is not overridden this time\n        with mock.patch('time.time', mock.Mock(return_value=21)):\n            p1['conflict'] = 1\n        p1.merge(p2.data)\n        self.assertEqual(p1, {'one': 1, 'two': 2, 'conflict': 1})\n"
  },
  {
    "path": "tox.ini",
    "content": "[testenv]\nusedevelop = true\ndeps =\n  coverage\nextras =\n  test\n  hub\nchangedir = {toxinidir}/tests\nsetenv =\n  HOME=/tmp\n  ELASTIC_HOST={env:ELASTIC_HOST:localhost}\ncommands =\n  orchstr8 download\n  blockchain: coverage run -p --source={envsitepackagesdir}/lbry -m unittest discover -vv integration.blockchain {posargs}\n  claims: coverage run -p --source={envsitepackagesdir}/lbry -m unittest discover -vv integration.claims {posargs}\n  takeovers: coverage run -p --source={envsitepackagesdir}/lbry -m unittest discover -vv integration.takeovers {posargs}\n  transactions: coverage run -p --source={envsitepackagesdir}/lbry -m unittest discover -vv integration.transactions {posargs}\n  datanetwork: coverage run -p --source={envsitepackagesdir}/lbry -m unittest discover -vv integration.datanetwork {posargs}\n  other: coverage run -p --source={envsitepackagesdir}/lbry -m unittest discover -vv integration.other {posargs}\n"
  }
]